前言
????前幾天有群友在群里問如何在我之前的文章《ASP.NET Core WebApi返回結(jié)果統(tǒng)一包裝實(shí)踐》的時(shí)候有點(diǎn)疑問,主要的疑問點(diǎn)就是關(guān)于Respouse的讀取的問題。在之前的文章《深入探究ASP.NET Core讀取Request.Body的正確方式》曾分析過關(guān)于Request的讀取問題,需要讀取Response的場(chǎng)景同樣經(jīng)常遇到,比如讀取輸出信息或者包裝一下輸出結(jié)果等。無獨(dú)有偶Response的讀取同樣存在類似的問題,本文我們便來分析一下如何進(jìn)行Response的Body讀取。
使用方式
我們?cè)谌粘5氖褂弥惺侨绾巫x取流呢?很簡單,直接使用StreamReader
去讀取,方式如下
public override void OnResultExecuted(ResultExecutedContext context)
{
//操作流之前恢復(fù)一下操作位
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);
}
代碼很簡單,直接讀取即可,可是這樣讀取是有問題的會(huì)拋出異常System.ArgumentException:“Stream was not readable.”
異常信息就是的意思是當(dāng)前Stream不可讀,也就是Respouse的Body是不可以被讀取的。關(guān)于StreamReader到底和Stream有啥關(guān)聯(lián),我們?cè)谥暗奈恼律钊胩骄緼SP.NET Core讀取Request.Body的正確方式一文中有過源碼分析,這里就不在贅述了,有興趣的同學(xué)可以自行翻閱,強(qiáng)烈建議在閱讀本文之前可以看一下那篇文章,方便更容易了解。
如何解決上面的問題呢?方式也很簡單,比如你想在你的程序中保證Response的Body都是可讀的,你可以定義一個(gè)中間件解決這個(gè)問題。
public static IApplicationBuilder UseResponseBodyRead(this IApplicationBuilder app)
{
return app.Use(async (context, next) =>
{
//獲取原始的Response Body
var originalResponseBody = context.Response.Body;
try
{
//聲明一個(gè)MemoryStream替換Response Body
using var swapStream = new MemoryStream();
context.Response.Body = swapStream;
await next(context);
//重置標(biāo)識(shí)位
context.Response.Body.Seek(0, SeekOrigin.Begin);
//把替換后的Response Body復(fù)制到原始的Response Body
await swapStream.CopyToAsync(originalResponseBody);
}
finally
{
//無論異常與否都要把原始的Body給切換回來
context.Response.Body = originalResponseBody;
}
});
}
本質(zhì)就是先用一個(gè)可操作的Stream比如咱們這里的MemoryStream
替換默認(rèn)的ResponseBody,讓后續(xù)對(duì)ResponseBody的操作都是針對(duì)新的ResponseBody進(jìn)行操作,完成之后把替換后的ResponseBody復(fù)制到原始的ResponseBody。最終無論異常與否都要把原始的Body給切換回來。需要注意的是,這個(gè)中間件的位置盡量要放在比較靠前的位置注冊(cè),至少也要保證在你所有要操作ResponseBody之前的位置注冊(cè)。如下所示
var app = builder.Build();
app.UseResponseBodyRead();
源碼探究
通過上面我們了解到了ResponseBody是不可以被讀取的,至于為什么呢,這個(gè)我們需要通過相關(guān)源碼了解一下。通過HttpContext
類的源碼我們可以看到相關(guān)定義
public abstract class HttpContext
{
public abstract HttpResponse Response { get; }
}
這里看到HttpContext
本身是個(gè)抽象類,看一下它的屬性HttpResponse
類的定義也是一個(gè)抽象類
public abstract class HttpResponse
{
}
由上面可知Response
屬性是抽象的,所以抽象類HttpResponse
必然包含一個(gè)子類去實(shí)現(xiàn)它,否則沒辦法直接操作相關(guān)方法。這里我們介紹一個(gè)網(wǎng)站https://source.dot.net用它可以更輕松的閱讀微軟類庫的源碼,比如CLR、ASP.NET Core、EF Core等等,雙擊一個(gè)類或者屬性方法可以查找引用和定義它們的地方,非常方便,它的源碼都是最新版本的,來源就是GitHub上的相關(guān)倉庫。找到實(shí)例化HttpResponse
的為位置在HttpContext
的子類DefaultHttpContext
類中[點(diǎn)擊查看源碼??]
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;
}
防止大家比較繞解釋一下,因?yàn)?code>HttpContext是抽象類,它包含了抽象屬性
HttpResponse
類型的屬性Response
,所以HttpContext
必然有子類去集成它,由于HttpResponse
也是抽象類,所以也必須包含了子類去繼承它。
尋找HttpResponse Body定義
通過上面的代碼我們可以看到HttpResponse
的子類為DefaultHttpResponse
類。找到類中Body
屬性定義的地方[點(diǎn)擊查看源碼??]看一下具體實(shí)現(xiàn)
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實(shí)例中查找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是抽象類,但是這里并沒有對(duì)Stream的子類直接進(jìn)行定義,而是引入了IHttpResponseBodyFeature
去和Stream交互,主要原因還是因?yàn)镽esponseBody涉及到一個(gè)交互體系,比如包含PipeWriter、SendFile等操作。所以這里我們只能順著IHttpResponseBodyFeature
的操作找到相關(guān)的實(shí)現(xiàn)類,通過查找引用關(guān)系我找到了實(shí)現(xiàn)類HttpProtocol
[點(diǎn)擊查看源碼??]我們看一下它的定義
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;
//省略一部分代碼
}
}
它實(shí)現(xiàn)了很多接口,其中包含了IHttpResponseBodyFeature
接口和IFeatureCollection
接口,這兩個(gè)接口在DefaultHttpResponse
類中都有涉獵,是Response輸出的交互類,可以理解為Response類是門面,實(shí)際的操作都是調(diào)用的具體類。我們可以分析一下包含獲取具體類型實(shí)例的操作,第一個(gè)便是它的索引器
操作
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相關(guān)的類來操作和獲取具體的相關(guān)的類型
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);
}
}
為什么會(huì)這樣的,相信大家已經(jīng)猜到了HttpProtocol
實(shí)現(xiàn)了很多的接口,意味著它有很多接口的能力。提供的這幾個(gè)方法可以根據(jù)類型快速的獲取想得到的實(shí)例。因?yàn)樵?code>HttpProtocol定義了許多變量承載它實(shí)現(xiàn)的接口的變量來承載當(dāng)前實(shí)例,所以在DefaultHttpResponse
看到了類似緩存的效果獲取具體接口的對(duì)應(yīng)實(shí)例。我們知道了HttpProtocol
實(shí)現(xiàn)了IHttpResponseBodyFeature
接口,所以我們?cè)?code>HttpProtocol類中查找給IHttpResponseBodyFeature的Stream
屬性賦值的地方即可,通過上面HttpProtocol
類的定義方式我們可以看到它是partial
也就是部分類,在另一個(gè)部分類中找到了賦值的地方[點(diǎn)擊查看源碼??]
Stream IHttpResponseBodyFeature.Stream => ResponseBody;
PipeWriter IHttpResponseBodyFeature.Writer => ResponseBodyPipeWriter;
Stream IHttpResponseFeature.Body
{
get => ResponseBody;
set => ResponseBody = value;
}
通過這個(gè)代碼我們可以看到IHttpResponseBodyFeature.Stream
來自ResponseBody
屬性,找到給HttpProtocol屬性ResponseBody
賦值的地方[點(diǎn)擊查看源碼??]
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
實(shí)例的Start方法里
這個(gè)方法傳遞的是當(dāng)前HttpProtocol實(shí)例
,所以直接找到BodyControl.Start方法
定義的地方[點(diǎn)擊查看源碼??]查看實(shí)現(xiàn)
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);
//實(shí)例化HttpResponseStream的地方
_response = new HttpResponseStream(bodyControl, _responseWriter);
}
public (Stream request, Stream response, PipeReader reader, PipeWriter writer) Start(MessageBody body)
{
//省略代碼
if (body.RequestUpgrade)
{
//默認(rèn)走不到暫時(shí)忽略
}
else if (body.ExtendedConnect)
{
//默認(rèn)走不到暫時(shí)忽略
}
else
{
//默認(rèn)走到這里
return (_request, _response, _requestReader, _responseWriter);
}
}
}
好了,饒了這么多的彎,我們水落石出了找到了HttpResponse.Body
的最終來源來自HttpResponseStream
類的實(shí)例。所以結(jié)論就是HttpResponse的Body是HttpResponseStream實(shí)例??偨Y(jié)一下
- HttpResponse的Body是Stream類型的,在DefaultHttpResponse中并未給Body直接賦值,而是在
IHttpResponseBodyFeature
實(shí)例中獲取Stream
屬性,這個(gè)類負(fù)責(zé)是ResponseBody相關(guān)的交互。 - IHttpResponseBodyFeature的實(shí)現(xiàn)類是
HttpProtocol
,這是一個(gè)部分類。在這里IHttpResponseBodyFeature.Stream
屬性來自HttpProtocol類ResponseBody
屬性。 - 給
HttpProtocol類ResponseBody
屬性賦值來自BodyControl的Start方法
,它返回的是BodyControl
類的_response
屬性,這個(gè)屬性的是HttpResponseStream
類型的。 - 所以得到結(jié)論
HttpResponse.Body
也就是Stream類型的,來自HttpResponseStream
類的實(shí)例。
HttpResponseStream類定義
上面饒了這么大的圈找到了HttpResponse.Body
實(shí)例的類型HttpResponseStream
類,找到類定義的地方看一下里面的實(shí)現(xiàn)[點(diǎn)擊查看源碼??]
internal sealed partial class HttpResponseStream : Stream
{
//說明不支持讀,如果想知道流是否可讀可以使用這個(gè)屬性先判斷
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);
//不可讀取和設(shè)置位置否則拋出異常
public override long Position
{
get => throw new NotSupportedException(SR.net_noseek);
set => throw new NotSupportedException(SR.net_noseek);
}
//不支持設(shè)置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);
//不支持讀讀相關(guān)的操作
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);
//省略寫相關(guān)方法和釋放相關(guān)的方法,只看設(shè)計(jì)到讀相關(guān)的地方
}
通過HttpResponseStream
類的定義我們可以看到,HttpResponseStream
本身是Stream
抽象類的子類。涉及到讀相關(guān)的方法是直接拋出異常,也就是最開始我們直接讀取HttpResponse.Body
讀取直接拋出異常的原因。不僅僅是讀取的方法不可用Postion、Length、Seek相關(guān)的方法都是不可操作的,操作了都會(huì)拋出異常。
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
默認(rèn)情況下是不可以讀取的,但是輸出Http日志時(shí)候是可以讀取ResponseBody
的,所以我們可以看一下里面的相關(guān)實(shí)現(xiàn),在HttpLoggingMiddleware
中間件里,因?yàn)檫@個(gè)中間件里涉及到Http日志記錄的相關(guān)邏輯實(shí)現(xiàn),而ResponseBody只是其中的一個(gè)選項(xiàng),所以咱們只關(guān)注這一部分的實(shí)現(xiàn)[點(diǎn)擊查看源碼??]
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>()!;
//實(shí)例化ResponseBufferingStream
responseBufferingStream = new ResponseBufferingStream(originalBodyFeature,
options.ResponseBodyLogLimit,
_logger,
context,
options.MediaTypeOptions.MediaTypeStates,
options);
//用ResponseBufferingStream實(shí)例替換原始ResponseBody
response.Body = responseBufferingStream;
//將responseBufferingStream設(shè)置到當(dāng)前的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);
}
}
通過上面的代碼我們可以看到,其實(shí)也是實(shí)現(xiàn)了類似的操作,用ResponseBufferingStream
替換掉原始的HttpResponseStream
類型,替換的邏輯要在中間件執(zhí)行next()
之前,操作完成之后也就是執(zhí)行了next()
之后再把原始的IHttpResponseBodyFeature
替換回來,有關(guān)具體的ResponseBufferingStream
實(shí)現(xiàn)方式咱們這里不做詳細(xì)描述了,不是本文重點(diǎn)。
ResponseBufferingStream
的實(shí)現(xiàn)并不是使用MemoryStream
這種可讀取的流替換掉默認(rèn)的HttpResponseStream,ResponseBufferingStream
的LogRequestBody()
方法使用ILogger
輸出日志并沒有直接去讀取Stream,而是反其道重寫了Stream的Write()
方法,因?yàn)閷?duì)HttpResponseBody實(shí)例HttpResponseStream
的輸出寫操作本質(zhì)是調(diào)用Stream的Write()
方法,重寫了Write()
方法之后會(huì)把寫入的內(nèi)容記錄到Buffer
中,LogRequestBody()
方法通過讀取Buffer中的內(nèi)容得到字符串,使用ILogger輸出日志。
答疑解惑
在之前的討論中有許多小伙伴對(duì)用MemoryStream
替換ResponseBody存在一個(gè)疑惑,就是既然已經(jīng)替換掉了,一直用MemoryStream
不就好了嘛,為啥還要把ResponseBody原始值記錄下來,結(jié)束后再替換回來。這個(gè)疑問咋一聽確實(shí)也沒毛病,但是等大致了解了它的使用過程之后才恍然大悟,原來是這么回事,在這里咱們就看一下為啥會(huì)是這樣。
首先說一下結(jié)論,如果把ResponseBody替換為MemoryStream
之后,不對(duì)原始的ResponseBody進(jìn)行操作的話,在這個(gè)中間件(類似上面說的到的UseResponseBodyRead中間件)之后的操作,可能是后續(xù)的其它中間件或者是各種終結(jié)點(diǎn)比如Controller的Action亦或者是MinimalApi的Map方法等,是可以讀取和寫入值的,也就是在替換中間件的范圍內(nèi),也就是大家經(jīng)常說的套娃模式,被它套進(jìn)去的是一直生效的,沒任何問題,終結(jié)點(diǎn)本身也是中間件
。下面這張圖相信大家經(jīng)常看到打個(gè)比方如果我的UseResponseBodyRead
中間件是圖里的Middleware1把ResponseBody替換為MemoryStream
,那么后續(xù)的操作比如Middleware2和Middleware3還有后續(xù)的終結(jié)點(diǎn)之類的讀取ResponseBody是完全沒有問題的。但是最終Http的輸出結(jié)果肯定是不符合預(yù)期的,這主要涉及到HttpResponseStream.Write()
的問題,我們知道最終我們輸出的結(jié)果會(huì)體現(xiàn)在Write()
方法上[點(diǎn)擊查看源碼??],核心代碼如下所示
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);
}
//調(diào)用WriteAsync方法
WriteAsync(buffer, offset, count, default).GetAwaiter().GetResult();
}
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
//本質(zhì)調(diào)用了HttpResponsePipeWriter的寫方法
return _pipeWriter.WriteAsync(new ReadOnlyMemory<byte>(buffer, offset, count), cancellationToken).GetAsTask();
}
)
通過上面我們可以看到HttpResponseStream
的Write()
方法本質(zhì)是調(diào)用了HttpResponsePipeWriter
的WriteAsync()
方法,HttpResponseStream本身不存儲(chǔ)寫入的數(shù)據(jù)。而HttpResponsePipeWriter
實(shí)例的構(gòu)建是在BodyControl
類中上面咱們已經(jīng)粘貼過實(shí)例化的源碼了,可自行翻閱上去看看HttpResponsePipeWriter
類的定義相關(guān)。所以上面把ResponseBody替換為MemoryStream
,最終的結(jié)果要體現(xiàn)在HttpResponseStream
實(shí)例中,否則的話沒有辦法正常輸出。可以用一個(gè)偽代碼例子演示一下這個(gè)原理
Order order1 = new Order
{
Address = "北京市海淀區(qū)"
};
SetOrder(order1);
Console.WriteLine($"最后地址:{order1.Address}");
public void SetOrder(Order order2)
{
order2 = new Order
{
Address = "上海市閔行區(qū)"
};
Console.WriteLine($"設(shè)置地址:{order2.Address}");
}
這個(gè)示例中即使SetOrder
方法中設(shè)置了新的Address,但是脫離了SetOrder
方法作用域后,外面的最后地址依然是北京市海淀區(qū)
。在調(diào)用SetOrder
進(jìn)入方法的時(shí)候order1和方法形參order2都指向的是Address = "北京市海淀區(qū)"
,在SetOrder方法內(nèi)部完成實(shí)例化之后order2指向的是Address = "上海市閔行區(qū)"
,但是order1依然指向的是Address = "北京市海淀區(qū)"
,因?yàn)橐脗鬟f形參本身只是存儲(chǔ)的引用地址,更換了引用地址就和原來的地址脫鉤了,如果想讓內(nèi)外行為一直必須要體現(xiàn)到原始值上面去。我們替換ResponseBody
的時(shí)候也是同理,最終Write本質(zhì)還是要依賴HttpResponseStream
里的HttpResponsePipeWriter
屬性,但是MemoryStream
可沒有HttpResponsePipeWriter
。你可能會(huì)有疑問,我上面也沒把MemoryStream
結(jié)果Write()
到HttpResponseStream
里去???但是上面使用了CopyToAsync
方法與原始的的ResponseBody類型HttpResponseStream
交互,CopyToAsync方法本質(zhì)就是在調(diào)用WriteAsync()
方法,口說無憑直接上代碼[點(diǎn)擊查看源碼??],核心代碼如下所示
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)
{
//使用了對(duì)象池復(fù)用空間
byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
try
{
int bytesRead;
while ((bytesRead = await source.ReadAsync(new Memory<byte>(buffer), cancellationToken).ConfigureAwait(false)) != 0)
{
//最終也是調(diào)用的目標(biāo)流的WriteAsync方法
await destination.WriteAsync(new ReadOnlyMemory<byte>(buffer, 0, bytesRead), cancellationToken).ConfigureAwait(false);
}
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
總結(jié)
????本文主要講解了如何讀取ResponseBody
,默認(rèn)情況下是不可以讀取的,需要我們使用了中間件結(jié)合MemoryStream
自行處理一下,同時(shí)我們結(jié)合和Http日志記錄中間件里的處理方式對(duì)比了一下,最終答疑了為了要把替換的結(jié)果還得繼續(xù)體現(xiàn)在原始的ResponseBody
上面去,整體來說這方面還是相對(duì)容易理解的,只是找起來可能比較麻煩。大致總結(jié)一下文章來源:http://www.zghlxwxcb.cn/news/detail-409348.html
- ResponseBody默認(rèn)不可讀取,因?yàn)樗膶?shí)例是
HttpResponseStream
這個(gè)類重寫了Stream的Read相關(guān)的方法,但是實(shí)現(xiàn)是拋出異常的,所以我們需要可讀的類來替換默認(rèn)的操作,MemoryStream
可以輔助實(shí)現(xiàn)。 -
UseHttpLogging
中間件也可以讀取ResponseBody里的結(jié)果,但是它是使用的重寫Stream的Write相關(guān)的方法,在Write方法里使用Buffer記錄了寫過的數(shù)據(jù),然后通過GetString()
方法讀取Buffer里的內(nèi)容實(shí)現(xiàn)記錄要輸出的值。 -
MemoryStream
解決的是我們?cè)趯懘a過程中對(duì)ResponseBody的讀取或?qū)懭氩僮鳎浅绦蛱幚硗曛笠?code>MemoryStream的結(jié)果在體現(xiàn)到HttpResponseStream
中去,否則雖然程序中讀取寫入Body沒問題,但是輸出的結(jié)果會(huì)出問題。
????說句題外話,ChatGTP
的發(fā)布對(duì)人們心里的沖擊還是挺大的,因?yàn)樗憩F(xiàn)出來的強(qiáng)大效果讓人眼前一亮,很多博主和企業(yè)也借此風(fēng)口尋找新的出路,甚至有人會(huì)擔(dān)心會(huì)不會(huì)被替代失業(yè)。個(gè)人以為新的技術(shù)大行其道必然會(huì)帶來新的產(chǎn)業(yè),新的產(chǎn)業(yè)的新的崗位同時(shí)也是需要更多的人參與進(jìn)來。所以保持對(duì)新事物的好奇心多多參與。工具不會(huì)替代人,能替代人的是會(huì)使用工具的人。
文章來源地址http://www.zghlxwxcb.cn/news/detail-409348.html

到了這里,關(guān)于由ASP.NET Core讀取Response.Body引發(fā)的思考的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!