由ASP.NET Core讀取Response.Body引發的思考

yi念之間發表於2023-04-10

前言

    前幾天有群友在群裡問如何在我之前的文章《ASP.NET Core WebApi返回結果統一包裝實踐》的時候有點疑問,主要的疑問點就是關於Respouse的讀取的問題。在之前的文章《深入探究ASP.NET Core讀取Request.Body的正確方式》曾分析過關於Request的讀取問題,需要讀取Response的場景同樣經常遇到,比如讀取輸出資訊或者包裝一下輸出結果等。無獨有偶Response的讀取同樣存在類似的問題,本文我們便來分析一下如何進行Response的Body讀取。

使用方式

我們在日常的使用中是如何讀取流呢?很簡單,直接使用StreamReader去讀取,方式如下

public override void OnResultExecuted(ResultExecutedContext context)
{
    //操作流之前恢復一下操作位
    context.HttpContext.Response.Body.Position = 0;

    StreamReader stream = new StreamReader(context.HttpContext.Response.Body);
    string body = stream.ReadToEnd();
    _logger.LogInformation("body content:" + body);

    context.HttpContext.Response.Body.Position = 0;
    base.OnResultExecuted(context);
}

程式碼很簡單,直接讀取即可,可是這樣讀取是有問題的會丟擲異常System.ArgumentException:“Stream was not readable.”異常資訊就是的意思是當前Stream不可讀,也就是Respouse的Body是不可以被讀取的。關於StreamReader到底和Stream有啥關聯,我們在之前的文章深入探究ASP.NET Core讀取Request.Body的正確方式一文中有過原始碼分析,這裡就不在贅述了,有興趣的同學可以自行翻閱,強烈建議在閱讀本文之前可以看一下那篇文章,方便更容易瞭解。
如何解決上面的問題呢?方式也很簡單,比如你想在你的程式中保證Response的Body都是可讀的,你可以定義一箇中介軟體解決這個問題。

public static IApplicationBuilder UseResponseBodyRead(this IApplicationBuilder app)
{
    return app.Use(async (context, next) =>
    {
        //獲取原始的Response Body
        var originalResponseBody = context.Response.Body;
        try
        {
            //宣告一個MemoryStream替換Response Body
            using var swapStream = new MemoryStream();
            context.Response.Body = swapStream;
            await next(context);
            //重置標識位
            context.Response.Body.Seek(0, SeekOrigin.Begin);
            //把替換後的Response Body複製到原始的Response Body
            await swapStream.CopyToAsync(originalResponseBody);
        }
        finally
        {
            //無論異常與否都要把原始的Body給切換回來
            context.Response.Body = originalResponseBody;
        }
    });
}

本質就是先用一個可操作的Stream比如我們們這裡的MemoryStream替換預設的ResponseBody,讓後續對ResponseBody的操作都是針對新的ResponseBody進行操作,完成之後把替換後的ResponseBody複製到原始的ResponseBody。最終無論異常與否都要把原始的Body給切換回來。需要注意的是,這個中介軟體的位置儘量要放在比較靠前的位置註冊,至少也要保證在你所有要操作ResponseBody之前的位置註冊。如下所示

var app = builder.Build();
app.UseResponseBodyRead();

原始碼探究

透過上面我們瞭解到了ResponseBody是不可以被讀取的,至於為什麼呢,這個我們需要透過相關原始碼瞭解一下。透過HttpContext類的原始碼我們可以看到相關定義

public abstract class HttpContext
{
    public abstract HttpResponse Response { get; }
}

這裡看到HttpContext本身是個抽象類,看一下它的屬性HttpResponse類的定義也是一個抽象類

public abstract class HttpResponse
{
}

由上面可知Response屬性是抽象的,所以抽象類HttpResponse必然包含一個子類去實現它,否則沒辦法直接操作相關方法。這裡我們介紹一個網站https://source.dot.net用它可以更輕鬆的閱讀微軟類庫的原始碼,比如CLR、ASP.NET Core、EF Core等等,雙擊一個類或者屬性方法可以查詢引用和定義它們的地方,非常方便,它的原始碼都是最新版本的,來源就是GitHub上的相關倉庫。找到例項化HttpResponse的為位置在HttpContext的子類DefaultHttpContext類中[點選檢視原始碼?]

public sealed class DefaultHttpContext : HttpContext
{
    private readonly DefaultHttpRequest _request;
    private readonly DefaultHttpResponse _response;

    public DefaultHttpContext(IFeatureCollection features)
    {
        _features.Initalize(features);
        _request = new DefaultHttpRequest(this);
        _response = new DefaultHttpResponse(this);
    }

    public override HttpRequest Request => _request;
    public override HttpResponse Response => _response;
}

防止大家比較繞解釋一下,因為HttpContext是抽象類,它包含了抽象屬性HttpResponse型別的屬性Response,所以HttpContext必然有子類去整合它,由於HttpResponse也是抽象類,所以也必須包含了子類去繼承它。

尋找HttpResponse Body定義

透過上面的程式碼我們可以看到HttpResponse的子類為DefaultHttpResponse類。找到類中Body屬性定義的地方[點選檢視原始碼?]看一下具體實現

internal sealed class DefaultHttpResponse : HttpResponse
{
    private static readonly Func<IFeatureCollection, IHttpResponseBodyFeature?> _nullResponseBodyFeature = f => null;

    private readonly DefaultHttpContext _context;
    private FeatureReferences<FeatureInterfaces> _features;

    public DefaultHttpResponse(DefaultHttpContext context)
    {
        _context = context;
        _features.Initalize(context.Features);
    }

    //在FeatureReferences<FeatureInterfaces>中取出ResponseBody的互動操作IHttpResponseBodyFeature
    private IHttpResponseBodyFeature HttpResponseBodyFeature => _features.Fetch(ref _features.Cache.ResponseBody, _nullResponseBodyFeature)!;

    //Body本身是Stream它是抽象類
    public override Stream Body
    {
        //在IHttpResponseBodyFeature例項中查詢Stream
        get { return HttpResponseBodyFeature.Stream; }
        set
        {
            var otherFeature = _features.Collection.GetRequiredFeature<IHttpResponseBodyFeature>();

            if (otherFeature is StreamResponseBodyFeature streamFeature
                && streamFeature.PriorFeature != null
                && object.ReferenceEquals(value, streamFeature.PriorFeature.Stream))
            {
                _features.Collection.Set(streamFeature.PriorFeature);
                return;
            }

            _features.Collection.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(value, otherFeature));
        }
    }
}

Body本身是Stream但是Stream是抽象類,但是這裡並沒有對Stream的子類直接進行定義,而是引入了IHttpResponseBodyFeature去和Stream互動,主要原因還是因為ResponseBody涉及到一個互動體系,比如包含PipeWriter、SendFile等操作。所以這裡我們只能順著IHttpResponseBodyFeature的操作找到相關的實現類,透過查詢引用關係我找到了實現類HttpProtocol[點選檢視原始碼?]我們看一下它的定義

 internal partial class HttpProtocol : IFeatureCollection,
                                          IHttpRequestFeature,
                                          IHttpResponseFeature,
                                          IHttpResponseBodyFeature,
                                          IRouteValuesFeature,
                                          IEndpointFeature,
                                          IHttpRequestIdentifierFeature,
                                          IHttpRequestTrailersFeature,
                                          IHttpExtendedConnectFeature,
                                          IHttpUpgradeFeature,
                                          IRequestBodyPipeFeature,
                                          IHttpConnectionFeature,
                                          IHttpRequestLifetimeFeature,
                                          IHttpBodyControlFeature,
                                          IHttpMaxRequestBodySizeFeature,
                                          IHttpRequestBodyDetectionFeature,
                                          IHttpWebTransportFeature,
                                          IBadRequestExceptionFeature
{
    internal protected IHttpResponseBodyFeature? _currentIHttpResponseBodyFeature;
    private void FastReset()
    {
        //省略一部分程式碼
        _currentIHttpResponseBodyFeature = this;
        //省略一部分程式碼
    }
}

它實現了很多介面,其中包含了IHttpResponseBodyFeature介面和IFeatureCollection介面,這兩個介面在DefaultHttpResponse類中都有涉獵,是Response輸出的互動類,可以理解為Response類是門面,實際的操作都是呼叫的具體類。我們可以分析一下包含獲取具體型別例項的操作,第一個便是它的索引器操作

internal protected IHttpResponseBodyFeature? _currentIHttpResponseBodyFeature;
object? IFeatureCollection.this[Type key]
{
    get
    {
        object? feature = null;
        //省略一部分程式碼
        if (key == typeof(IHttpResponseBodyFeature))
        {
            feature = _currentIHttpResponseBodyFeature;
        }
        //省略一部分程式碼
        return feature ?? ConnectionFeatures?[key];
    }
    set
    {
        _featureRevision++;
        //省略一部分程式碼
        if (key == typeof(IHttpResponseBodyFeature))
        {
            _currentIHttpResponseBodyFeature = (IHttpResponseBodyFeature?)value;
        }
        //省略一部分程式碼
    }
}

它本身也提供Get和Set相關的類來操作和獲取具體的相關的型別

TFeature? IFeatureCollection.Get<TFeature>() where TFeature : default
{
    TFeature? feature = default;
    if (typeof(TFeature) == typeof(IHttpResponseBodyFeature))
    {
        feature = Unsafe.As<IHttpResponseBodyFeature?, TFeature?>(ref _currentIHttpResponseBodyFeature);
    }
    return feature;
}

void IFeatureCollection.Set<TFeature>(TFeature? feature) where TFeature : default
{
    _featureRevision++;
    if (typeof(TFeature) == typeof(IHttpResponseBodyFeature))
    {
        _currentIHttpResponseBodyFeature = Unsafe.As<TFeature?, IHttpResponseBodyFeature?>(ref feature);
    }
}

為什麼會這樣的,相信大家已經猜到了HttpProtocol實現了很多的介面,意味著它有很多介面的能力。提供的這幾個方法可以根據型別快速的獲取想得到的例項。因為在HttpProtocol定義了許多變數承載它實現的介面的變數來承載當前例項,所以在DefaultHttpResponse看到了類似快取的效果獲取具體介面的對應例項。我們知道了HttpProtocol實現了IHttpResponseBodyFeature介面,所以我們在HttpProtocol類中查詢給IHttpResponseBodyFeature的Stream屬性賦值的地方即可,透過上面HttpProtocol類的定義方式我們可以看到它是partial也就是部分類,在另一個部分類中找到了賦值的地方[點選檢視原始碼?]

Stream IHttpResponseBodyFeature.Stream => ResponseBody;
PipeWriter IHttpResponseBodyFeature.Writer => ResponseBodyPipeWriter;

Stream IHttpResponseFeature.Body
{
    get => ResponseBody;
    set => ResponseBody = value;
}

透過這個程式碼我們可以看到IHttpResponseBodyFeature.Stream來自ResponseBody屬性,找到給HttpProtocol屬性ResponseBody賦值的地方[點選檢視原始碼?]

protected BodyControl? _bodyControl;
public Stream ResponseBody { get; set; } = default!;
public PipeWriter ResponseBodyPipeWriter { get; set; } = default!;
public void InitializeBodyControl(MessageBody messageBody)
{
    if (_bodyControl == null)
    {
        _bodyControl = new BodyControl(bodyControl: this, this);
    }

    (RequestBody, ResponseBody, RequestBodyPipeReader, ResponseBodyPipeWriter) = _bodyControl.Start(messageBody);
}

上面的程式碼我們可以看到ResponseBody定義和賦值的地方,我們可以看到給ResponseBody賦值來自BodyControl例項的Start方法裡這個方法傳遞的是當前HttpProtocol例項,所以直接找到BodyControl.Start方法定義的地方[點選檢視原始碼?]檢視實現

internal sealed class BodyControl
{
    //HttpResponseStream
    private readonly HttpResponseStream _response;
    private readonly HttpResponsePipeWriter _responseWriter;

    private readonly HttpRequestPipeReader _requestReader;
    private readonly HttpRequestStream _request;

    public BodyControl(IHttpBodyControlFeature bodyControl, IHttpResponseControl responseControl)
    {
        _requestReader = new HttpRequestPipeReader();
        _request = new HttpRequestStream(bodyControl, _requestReader);

        _responseWriter = new HttpResponsePipeWriter(responseControl);
        //例項化HttpResponseStream的地方
        _response = new HttpResponseStream(bodyControl, _responseWriter);
    }

    public (Stream request, Stream response, PipeReader reader, PipeWriter writer) Start(MessageBody body)
    {
        //省略程式碼
        if (body.RequestUpgrade)
        {
        //預設走不到暫時忽略
        }
        else if (body.ExtendedConnect)
        {
        //預設走不到暫時忽略
        }
        else
        {
            //預設走到這裡
            return (_request, _response, _requestReader, _responseWriter);
        }
    }
}

好了,饒了這麼多的彎,我們水落石出了找到了HttpResponse.Body的最終來源來自HttpResponseStream類的例項。所以結論就是HttpResponse的Body是HttpResponseStream例項。總結一下

  • HttpResponse的Body是Stream型別的,在DefaultHttpResponse中並未給Body直接賦值,而是在IHttpResponseBodyFeature例項中獲取Stream屬性,這個類負責是ResponseBody相關的互動。
  • IHttpResponseBodyFeature的實現類是HttpProtocol,這是一個部分類。在這裡IHttpResponseBodyFeature.Stream屬性來自HttpProtocol類ResponseBody屬性。
  • HttpProtocol類ResponseBody屬性賦值來自BodyControl的Start方法,它返回的是BodyControl類的_response屬性,這個屬性的是HttpResponseStream型別的。
  • 所以得到結論HttpResponse.Body也就是Stream型別的,來自HttpResponseStream類的例項。

HttpResponseStream類定義

上面饒了這麼大的圈找到了HttpResponse.Body例項的型別HttpResponseStream類,找到類定義的地方看一下里面的實現[點選檢視原始碼?]

internal sealed partial class HttpResponseStream : Stream
{
    //說明不支援讀,如果想知道流是否可讀可以使用這個屬性先判斷
    public override bool CanRead => false;
    //流不可查詢
    public override bool CanSeek => false;
    //支援寫
    public override bool CanWrite => true;
    //不能獲取流的長度否則丟擲異常
    public override long Length => throw new NotSupportedException(SR.net_noseek);

    //不可讀取和設定位置否則丟擲異常
    public override long Position
    {
        get => throw new NotSupportedException(SR.net_noseek);
        set => throw new NotSupportedException(SR.net_noseek);
    }
    //不支援設定Seek否則丟擲異常
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(SR.net_noseek);
    //不支援Length否則丟擲異常
    public override void SetLength(long value) => throw new NotSupportedException(SR.net_noseek);
    //不支援讀取操作否則丟擲異常
    public override int Read(byte[] buffer, int offset, int size) => throw new InvalidOperationException(SR.net_writeonlystream);
    //不支援讀讀相關的操作
    public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback? callback, object? state)
    {
        throw new InvalidOperationException(SR.net_writeonlystream);
    }
    public override int EndRead(IAsyncResult asyncResult) => throw new InvalidOperationException(SR.net_writeonlystream);

    //省略寫相關方法和釋放相關的方法,只看設計到讀相關的地方
}

透過HttpResponseStream類的定義我們可以看到,HttpResponseStream本身是Stream抽象類的子類。涉及到讀相關的方法是直接丟擲異常,也就是最開始我們直接讀取HttpResponse.Body讀取直接丟擲異常的原因。不僅僅是讀取的方法不可用Postion、Length、Seek相關的方法都是不可操作的,操作了都會丟擲異常。

UseHttpLogging的解決方式

從ASP.NET Core6.0之後開始,推出了HTTP日誌記錄功能,使用方式如下

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddHttpLogging(logging =>
{
    logging.LoggingFields = HttpLoggingFields.ResponseBody;
    logging.RequestBodyLogLimit = 4096;
});

var app = builder.Build();
app.UseHttpLogging();

不過我們透過上面看到了HttpResponse.Body預設情況下是不可以讀取的,但是輸出Http日誌時候是可以讀取ResponseBody的,所以我們可以看一下里面的相關實現,在HttpLoggingMiddleware中介軟體裡,因為這個中介軟體裡涉及到Http日誌記錄的相關邏輯實現,而ResponseBody只是其中的一個選項,所以我們們只關注這一部分的實現[點選檢視原始碼?]

ResponseBufferingStream? responseBufferingStream = null;
IHttpResponseBodyFeature? originalBodyFeature = null;
try
{
    //獲取原始的response
    var response = context.Response;

    if (options.LoggingFields.HasFlag(HttpLoggingFields.ResponseBody))
    {
        //儲存原始的IHttpResponseBodyFeature也就是上面提到的ResponseBody互動類
        originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>()!;
        //例項化ResponseBufferingStream
        responseBufferingStream = new ResponseBufferingStream(originalBodyFeature,
            options.ResponseBodyLogLimit,
            _logger,
            context,
            options.MediaTypeOptions.MediaTypeStates,
            options);
        //用ResponseBufferingStream例項替換原始ResponseBody
        response.Body = responseBufferingStream;
        //將responseBufferingStream設定到當前的IHttpResponseBodyFeature
        context.Features.Set<IHttpResponseBodyFeature>(responseBufferingStream);
    }

    await _next(context);

    //輸出日誌
    if (requestBufferingStream?.HasLogged == false)
    {
        requestBufferingStream.LogRequestBody();
    }
    if (responseBufferingStream != null)
    {
        var responseBody = responseBufferingStream.GetString(responseBufferingStream.Encoding);
        if (!string.IsNullOrEmpty(responseBody))
        {
            _logger.ResponseBody(responseBody);
        }
    }
}
finally
{
    responseBufferingStream?.Dispose();
    if (originalBodyFeature != null)
    {
        //還原原始的IHttpResponseBodyFeature
        context.Features.Set(originalBodyFeature);
    }
}

透過上面的程式碼我們可以看到,其實也是實現了類似的操作,用ResponseBufferingStream替換掉原始的HttpResponseStream型別,替換的邏輯要在中介軟體執行next()之前,操作完成之後也就是執行了next()之後再把原始的IHttpResponseBodyFeature替換回來,有關具體的ResponseBufferingStream實現方式我們們這裡不做詳細描述了,不是本文重點。

ResponseBufferingStream的實現並不是使用MemoryStream這種可讀取的流替換掉預設的HttpResponseStream,ResponseBufferingStreamLogRequestBody()方法使用ILogger輸出日誌並沒有直接去讀取Stream,而是反其道重寫了Stream的Write()方法,因為對HttpResponseBody例項
HttpResponseStream的輸出寫操作本質是呼叫Stream的Write()方法,重寫了Write()方法之後會把寫入的內容記錄到Buffer中,LogRequestBody()方法透過讀取Buffer中的內容得到字串,使用ILogger輸出日誌。

答疑解惑

在之前的討論中有許多小夥伴對用MemoryStream替換ResponseBody存在一個疑惑,就是既然已經替換掉了,一直用MemoryStream不就好了嘛,為啥還要把ResponseBody原始值記錄下來,結束後再替換回來。這個疑問咋一聽確實也沒毛病,但是等大致瞭解了它的使用過程之後才恍然大悟,原來是這麼回事,在這裡我們們就看一下為啥會是這樣。
首先說一下結論,如果把ResponseBody替換為MemoryStream之後,不對原始的ResponseBody進行操作的話,在這個中介軟體(類似上面說的到的UseResponseBodyRead中介軟體)之後的操作,可能是後續的其它中介軟體或者是各種終結點比如Controller的Action亦或者是MinimalApi的Map方法等,是可以讀取和寫入值的,也就是在替換中介軟體的範圍內,也就是大家經常說的套娃模式,被它套進去的是一直生效的,沒任何問題,終結點本身也是中介軟體。下面這張圖相信大家經常看到ASP.NET Core中介軟體打個比方如果我的UseResponseBodyRead中介軟體是圖裡的Middleware1把ResponseBody替換為MemoryStream,那麼後續的操作比如Middleware2和Middleware3還有後續的終結點之類的讀取ResponseBody是完全沒有問題的。但是最終Http的輸出結果肯定是不符合預期的,這主要涉及到HttpResponseStream.Write()的問題,我們知道最終我們輸出的結果會體現在Write()方法上[點選檢視原始碼?],核心程式碼如下所示

internal sealed class HttpResponseStream : Stream
{
    private readonly HttpResponsePipeWriter _pipeWriter;
    private readonly IHttpBodyControlFeature _bodyControl;

    public HttpResponseStream(IHttpBodyControlFeature bodyControl, HttpResponsePipeWriter pipeWriter)
    {
        _bodyControl = bodyControl;
        _pipeWriter = pipeWriter;
    }

    //重寫Stream的Write操作
    public override void Write(byte[] buffer, int offset, int count)
    {
        if (!_bodyControl.AllowSynchronousIO)
        {
            throw new InvalidOperationException(CoreStrings.SynchronousWritesDisallowed);
        }
        //呼叫WriteAsync方法
        WriteAsync(buffer, offset, count, default).GetAwaiter().GetResult();
    }

    public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
    {
        //本質呼叫了HttpResponsePipeWriter的寫方法
        return _pipeWriter.WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken).GetAsTask();
    }
)

透過上面我們可以看到HttpResponseStreamWrite()方法本質是呼叫了HttpResponsePipeWriterWriteAsync()方法,HttpResponseStream本身不儲存寫入的資料。而HttpResponsePipeWriter例項的構建是在BodyControl類中上面我們們已經貼上過例項化的原始碼了,可自行翻閱上去看看HttpResponsePipeWriter類的定義相關。所以上面把ResponseBody替換為MemoryStream,最終的結果要體現在HttpResponseStream例項中,否則的話沒有辦法正常輸出。可以用一個虛擬碼例子演示一下這個原理

Order order1 = new Order 
{
    Address = "北京市海淀區"
};

SetOrder(order1);

Console.WriteLine($"最後地址:{order1.Address}");

public void SetOrder(Order order2)
{
    order2 = new Order
    {
        Address = "上海市閔行區"
    };
    Console.WriteLine($"設定地址:{order2.Address}");
}

這個示例中即使SetOrder方法中設定了新的Address,但是脫離了SetOrder方法作用域後,外面的最後地址依然是北京市海淀區。在呼叫SetOrder進入方法的時候order1和方法形參order2都指向的是Address = "北京市海淀區",在SetOrder方法內部完成例項化之後order2指向的是Address = "上海市閔行區",但是order1依然指向的是Address = "北京市海淀區",因為引用傳遞形參本身只是儲存的引用地址,更換了引用地址就和原來的地址脫鉤了,如果想讓內外行為一直必須要體現到原始值上面去。我們替換ResponseBody的時候也是同理,最終Write本質還是要依賴HttpResponseStream裡的HttpResponsePipeWriter屬性,但是MemoryStream可沒有HttpResponsePipeWriter。你可能會有疑問,我上面也沒把MemoryStream結果Write()HttpResponseStream裡去啊?但是上面使用了CopyToAsync方法與原始的的ResponseBody型別HttpResponseStream互動,CopyToAsync方法本質就是在呼叫WriteAsync()方法,口說無憑直接上程式碼[點選檢視原始碼?],核心程式碼如下所示

public virtual Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
{
    //省略一部分程式碼

    return Core(this, destination, bufferSize, cancellationToken);

    static async Task Core(Stream source, Stream destination, int bufferSize, CancellationToken cancellationToken)
    {
        //使用了物件池複用空間
        byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
        try
        {
            int bytesRead;
            while ((bytesRead = await source.ReadAsync(new Memory<byte>(buffer), cancellationToken).ConfigureAwait(false)) != 0)
            {
                //最終也是呼叫的目標流的WriteAsync方法
                await destination.WriteAsync(new ReadOnlyMemory<byte>(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false);
            }
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }
}

總結

    本文主要講解了如何讀取ResponseBody,預設情況下是不可以讀取的,需要我們使用了中介軟體結合MemoryStream自行處理一下,同時我們結合和Http日誌記錄中介軟體裡的處理方式對比了一下,最終答疑了為了要把替換的結果還得繼續體現在原始的ResponseBody上面去,整體來說這方面還是相對容易理解的,只是找起來可能比較麻煩。大致總結一下

  • ResponseBody預設不可讀取,因為它的例項是HttpResponseStream這個類重寫了Stream的Read相關的方法,但是實現是丟擲異常的,所以我們需要可讀的類來替換預設的操作,MemoryStream可以輔助實現。
  • UseHttpLogging中介軟體也可以讀取ResponseBody裡的結果,但是它是使用的重寫Stream的Write相關的方法,在Write方法裡使用Buffer記錄了寫過的資料,然後透過GetString()方法讀取Buffer裡的內容實現記錄要輸出的值。
  • MemoryStream解決的是我們在寫程式碼過程中對ResponseBody的讀取或寫入操作,但是程式處理完之後要把MemoryStream的結果在體現到HttpResponseStream中去,否則雖然程式中讀取寫入Body沒問題,但是輸出的結果會出問題。

    說句題外話,ChatGTP的釋出對人們心裡的衝擊還是挺大的,因為它表現出來的強大效果讓人眼前一亮,很多博主和企業也藉此風口尋找新的出路,甚至有人會擔心會不會被替代失業。個人以為新的技術大行其道必然會帶來新的產業,新的產業的新的崗位同時也是需要更多的人參與進來。所以保持對新事物的好奇心多多參與。工具不會替代人,能替代人的是會使用工具的人。

?歡迎掃碼關注我的公眾號? 由ASP.NET Core讀取Response.Body引發的思考

相關文章