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

前言

    前幾天有羣友在羣裏問如何在我之前的文章《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的發佈對人們心裏的衝擊還是挺大的,因爲它表現出來的強大效果讓人眼前一亮,很多博主和企業也藉此風口尋找新的出路,甚至有人會擔心會不會被替代失業。個人以爲新的技術大行其道必然會帶來新的產業,新的產業的新的崗位同時也是需要更多的人蔘與進來。所以保持對新事物的好奇心多多參與。工具不會替代人,能替代人的是會使用工具的人。

👇歡迎掃碼關注我的公衆號👇
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章