ASP.NET CoreアクションからResponse.Body
プロパティを取得するのに苦労してきましたが、特定できた唯一のソリューションは最適ではないようです。このソリューションでは、ストリームを文字列変数に読み取り中にResponse.Body
をMemoryStream
と交換し、その後、クライアントに送信する前に交換する必要があります。以下の例では、カスタムミドルウェアクラスでResponse.Body
値を取得しようとしています。 Response.Body
は、何らかの理由でASP.NET Coreのsetのみのプロパティですか?私はここで何かを見逃していますか、これは監視/バグ/設計の問題ですか? Response.Body
を読むより良い方法はありますか?
現在の(次善の)ソリューション:
public class MyMiddleWare
{
private readonly RequestDelegate _next;
public MyMiddleWare(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
using (var swapStream = new MemoryStream())
{
var originalResponseBody = context.Response.Body;
context.Response.Body = swapStream;
await _next(context);
swapStream.Seek(0, SeekOrigin.Begin);
string responseBody = new StreamReader(swapStream).ReadToEnd();
swapStream.Seek(0, SeekOrigin.Begin);
await swapStream .CopyToAsync(originalResponseBody);
context.Response.Body = originalResponseBody;
}
}
}
EnableRewind()を使用した解決策の試み:これはRequest.Body
ではなくResponse.Body
でのみ機能します。これにより、実際の応答本文の内容ではなく、Response.Body
から空の文字列が読み取られます。
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifeTime)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.Use(async (context, next) => {
context.Request.EnableRewind();
await next();
});
app.UseMyMiddleWare();
app.UseMvc();
// Dispose of Autofac container on application stop
appLifeTime.ApplicationStopped.Register(() => this.ApplicationContainer.Dispose());
}
MyMiddleWare.cs
public class MyMiddleWare
{
private readonly RequestDelegate _next;
public MyMiddleWare(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
await _next(context);
string responseBody = new StreamReader(context.Request.Body).ReadToEnd(); //responseBody is ""
context.Request.Body.Position = 0;
}
}
私の元の応答では、質問を完全に読み違えて、ポスターがRequest.Body
の読み方を尋ねていると思いましたが、彼はResponse.Body
の読み方を尋ねました。履歴を保存するために元の回答を残しますが、それを更新して、問題を正しく読んだ後にどのように回答するかを示します。
元の回答
複数回の読み取りをサポートするバッファリングされたストリームが必要な場合は、設定する必要があります
context.Request.EnableRewind()
理想的には、何かを読む必要がある前に、ミドルウェアの早い段階でこれを行います。
したがって、たとえば、Startup.csファイルのConfigure
メソッドの先頭に次のコードを配置できます。
app.Use(async (context, next) => {
context.Request.EnableRewind();
await next();
});
巻き戻しを有効にする前は、Request.Body
に関連付けられたストリームは、2回目のストリームのシークまたは読み取りをサポートしない順方向専用ストリームです。これは、リクエスト処理のデフォルト設定を可能な限り軽量かつ高性能にするために行われました。ただし、巻き戻しを有効にすると、ストリームはシークと読み取りを複数回サポートするストリームにアップグレードされます。 EnableRewind
の呼び出しの直前と直後にブレークポイントを設定し、Request.Body
プロパティを監視することにより、この「アップグレード」を監視できます。たとえば、Request.Body.CanSeek
はfalse
からtrue
に変更されます。
update:ASP.NET Core 2.1以降では、Request.Body
をFileBufferingReadStream
にアップグレードするRequest.EnableBuffering()
が利用可能ですRequest.EnableRewind()
などであり、Request.EnableBuffering()
は内部名前空間ではなくパブリック名前空間にあるため、EnableRewind()よりも優先されるべきです。 (指摘してくれた@ArjanEinbuに感謝)
次に、ボディストリームを読み取るには、たとえば次のようにします。
string bodyContent = new StreamReader(Request.Body).ReadToEnd();
ただし、usingステートメントでStreamReader
の作成をラップしないでください。そうしないと、usingブロックの終わりに基になるボディストリームが閉じ、リクエストライフサイクルの後半でコードがボディを読み取ることができなくなります。
また、念のため、このコード行で本文の内容を読み取る上記のコード行に従って、本文のストリーム位置を0にリセットすることをお勧めします。
request.Body.Position = 0;
こうすることで、リクエストライフサイクルの後半のコードは、まだ読み取られていない状態のrequest.Bodyを見つけます。
更新された回答
もともとあなたの質問を読み違えました。関連付けられたストリームをバッファリングされたストリームにアップグレードするという概念は引き続き適用されます。ただし、手動で行う必要がありますが、EnableRewind()
が読み取られた後に開発者が要求ストリームを再読み取りできるように、一度書き込まれた応答ストリームを読み取ることができる組み込みの.Net Core機能を認識していません。
あなたの「ハッキング」アプローチはおそらく完全に適切です。基本的に、シークできないストリームを、できるストリームに変換しています。一日の終わりには、Response.Body
ストリームをバッファリングされたシークをサポートするストリームと交換する必要があります。これを行うためのミドルウェアの別の例がありますが、それはあなたのアプローチに非常に似ていることに気付くでしょう。ただし、元のストリームをResponse.Body
に戻すための追加の保護としてfinallyブロックを使用することを選択しました。また、構文以来Position
メソッドではなく、ストリームのSeek
プロパティを使用しました少し簡単ですが、効果はあなたのアプローチと変わりません。
public class ResponseRewindMiddleware {
private readonly RequestDelegate next;
public ResponseRewindMiddleware(RequestDelegate next) {
this.next = next;
}
public async Task Invoke(HttpContext context) {
Stream originalBody = context.Response.Body;
try {
using (var memStream = new MemoryStream()) {
context.Response.Body = memStream;
await next(context);
memStream.Position = 0;
string responseBody = new StreamReader(memStream).ReadToEnd();
memStream.Position = 0;
await memStream.CopyToAsync(originalBody);
}
} finally {
context.Response.Body = originalBody;
}
}
ハックとは、実際にはカスタムミドルウェアで応答ストリームを管理する方法の推奨されるアプローチです。
ミドルウェア設計のパイプラインの性質のため、各ミドルウェアはパイプラインの前または次のハンドラーを認識しません。現在のミドルウェアが、それが制御するストリームを渡す前に与えられた応答ストリームを保持しない限り、現在のミドルウェアが応答を書き込むという保証はありません。この設計はOWINで見られ、最終的にasp.net-coreに焼き付けられました。
応答ストリームへの書き込みを開始すると、クライアントに本文とヘッダー(応答)を送信します。パイプラインを下る別のハンドラーが現在のハンドラーがチャンスを得る前にそれを行った場合、既に送信された応答に何も追加することはできません。
パイプライン内の前のミドルウェアが別のストリームをラインに渡すという同じ戦略に従った場合、これも実際の応答ストリームであることが保証されません。
参照 ASP.NET Core Middleware Fundamentals
警告
HttpResponse
を呼び出した後、next
を慎重に変更してください。応答が既にクライアントに送信されている可能性があるためです。 HttpResponse.HasStarted を使用して、ヘッダーが送信されたかどうかを確認できます。警告
write
メソッドを呼び出した後にnext.Invoke
を呼び出さないでください。ミドルウェアコンポーネントは、応答を生成するか、next.Invoke
を呼び出しますが、両方は呼び出しません。
aspnet/BasicMiddlewareGithubリポジトリからの組み込みの基本ミドルウェアの例
ResponseCompressionMiddleware.cs
/// <summary>
/// Invoke the middleware.
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public async Task Invoke(HttpContext context)
{
if (!_provider.CheckRequestAcceptsCompression(context))
{
await _next(context);
return;
}
var bodyStream = context.Response.Body;
var originalBufferFeature = context.Features.Get<IHttpBufferingFeature>();
var originalSendFileFeature = context.Features.Get<IHttpSendFileFeature>();
var bodyWrapperStream = new BodyWrapperStream(context, bodyStream, _provider,
originalBufferFeature, originalSendFileFeature);
context.Response.Body = bodyWrapperStream;
context.Features.Set<IHttpBufferingFeature>(bodyWrapperStream);
if (originalSendFileFeature != null)
{
context.Features.Set<IHttpSendFileFeature>(bodyWrapperStream);
}
try
{
await _next(context);
// This is not disposed via a using statement because we don't want to flush the compression buffer for unhandled exceptions,
// that may cause secondary exceptions.
bodyWrapperStream.Dispose();
}
finally
{
context.Response.Body = bodyStream;
context.Features.Set(originalBufferFeature);
if (originalSendFileFeature != null)
{
context.Features.Set(originalSendFileFeature);
}
}
}
リクエストとレスポンスを記録するために、リクエストパイプラインで ミドルウェア を使用できます。
ただし、次の事実により、memory leak
の危険性が増加します。1。ストリーム、2。バイトバッファの設定、3。文字列変換
Large Object Heap (要求または応答の本文が85,000バイトを超える場合)に達する可能性があります。これにより、アプリケーションのメモリリークの危険性が高まります。 LOHを回避するために、関連する library を使用して、メモリストリームを Recyclable Memory stream に置き換えることができます。
リサイクル可能なメモリストリームを使用する実装:
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private const int ReadChunkBufferLength = 4096;
public RequestResponseLoggingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory)
{
_next = next;
_logger = loggerFactory
.CreateLogger<RequestResponseLoggingMiddleware>();
_recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
}
public async Task Invoke(HttpContext context)
{
LogRequest(context.Request);
await LogResponseAsync(context);
}
private void LogRequest(HttpRequest request)
{
request.EnableRewind();
using (var requestStream = _recyclableMemoryStreamManager.GetStream())
{
request.Body.CopyTo(requestStream);
_logger.LogInformation($"Http Request Information:{Environment.NewLine}" +
$"Schema:{request.Scheme} " +
$"Host: {request.Host} " +
$"Path: {request.Path} " +
$"QueryString: {request.QueryString} " +
$"Request Body: {ReadStreamInChunks(requestStream)}");
}
}
private async Task LogResponseAsync(HttpContext context)
{
var originalBody = context.Response.Body;
using (var responseStream = _recyclableMemoryStreamManager.GetStream())
{
context.Response.Body = responseStream;
await _next.Invoke(context);
await responseStream.CopyToAsync(originalBody);
_logger.LogInformation($"Http Response Information:{Environment.NewLine}" +
$"Schema:{context.Request.Scheme} " +
$"Host: {context.Request.Host} " +
$"Path: {context.Request.Path} " +
$"QueryString: {context.Request.QueryString} " +
$"Response Body: {ReadStreamInChunks(responseStream)}");
}
context.Response.Body = originalBody;
}
private static string ReadStreamInChunks(Stream stream)
{
stream.Seek(0, SeekOrigin.Begin);
string result;
using (var textWriter = new StringWriter())
using (var reader = new StreamReader(stream))
{
var readChunk = new char[ReadChunkBufferLength];
int readChunkLength;
//do while: is useful for the last iteration in case readChunkLength < chunkLength
do
{
readChunkLength = reader.ReadBlock(readChunk, 0, ReadChunkBufferLength);
textWriter.Write(readChunk, 0, readChunkLength);
} while (readChunkLength > 0);
result = textWriter.ToString();
}
return result;
}
}
NB。 textWriter.ToString()
により、LOHの危険は完全に根絶されません。一方、構造化されたログ(Serilogなど)をサポートするログクライアントライブラリを使用して、リサイクル可能なメモリストリームのインスタンスを挿入できます。