前言
前幾天有群友在群裡問如何在我之前的文章《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,ResponseBufferingStream
的LogRequestBody()
方法使用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方法等,是可以讀取和寫入值的,也就是在替換中介軟體的範圍內,也就是大家經常說的套娃模式,被它套進去的是一直生效的,沒任何問題,終結點本身也是中介軟體
。下面這張圖相信大家經常看到打個比方如果我的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();
}
)
透過上面我們可以看到HttpResponseStream
的Write()
方法本質是呼叫了HttpResponsePipeWriter
的WriteAsync()
方法,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
的釋出對人們心裡的衝擊還是挺大的,因為它表現出來的強大效果讓人眼前一亮,很多博主和企業也藉此風口尋找新的出路,甚至有人會擔心會不會被替代失業。個人以為新的技術大行其道必然會帶來新的產業,新的產業的新的崗位同時也是需要更多的人參與進來。所以保持對新事物的好奇心多多參與。工具不會替代人,能替代人的是會使用工具的人。