ASP.NET CoreのアクションからResponse.Body
プロパティを取得するのに苦労していますが、私が特定できた唯一の解決策は最適とは言えないようです。 その解決策は、Response.Body
を MemoryStream
と交換し、そのストリームを文字列変数に読み込んで、クライアントに送信する前にそれを元に戻す必要があります。 以下の例では、カスタムミドルウェアクラスで Response.Body
の値を取得しようとしています。 ASP.NET Coreでは、なぜか Response.Body
は set のみのプロパティになっていますね? それとも見落とし/バグ/設計上の問題なのでしょうか? それとも、見落とし/バグ/デザインの問題でしょうか?
現在の(最適ではない)解決策:。
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();
});
Rewind を有効にする前は、Request.Body
に関連付けられたストリームは転送専用で、2 回目のシークや読み込みはサポートされていません。 これは、リクエスト処理のデフォルトの構成を、可能な限り軽量で高性能なものにするために行われました。 しかし、一旦巻き戻しを有効にすると、ストリームは複数回のシークと読み込みをサポートするストリームにアップグレードされます。 この "upgrade" は、 EnableRewind
の呼び出しの直前と直後にブレークポイントを設定し、 Request.Body
のプロパティを観測することで確認できます。 例えば、 Request.Body.CanSeek
は false
から true
に変更されます。
update:ASP.NET Core 2.1 からは Request.EnableBuffering()
が利用可能になりました。これは Request.Body
を Request.EnableRewind()
と同じように FileBufferingReadStream
にアップグレードします。Request.EnableBuffering()
は内部ではなくパブリック名前空間にあるので EnableRewind() より推奨されるべきです。 (指摘してくれた @ArjanEinbu に感謝します)
そして、ボディストリームを読むには、例えば次のようにします。
string bodyContent = new StreamReader(Request.Body).ReadToEnd();
さもないと、using ブロックの終了時に基礎となるボディストリームが閉じられ、リクエストライフサイクルの後半にあるコードではボディを読むことができなくなります。
また、念のため、ボディの内容を読み取る上記のコード行の後に、ボディのストリーム位置を0に戻すこのコード行を追加するとよいかもしれません。
request.Body.Position = 0;
そうすれば、リクエストのライフサイクルの後半にあるどんなコードも、まだ読まれていないような状態のrequest.Bodyを見つけることができます。
回答更新
当初、質問を読み違えてしまい申し訳ありません。関連するストリームをバッファードストリームにアップグレードするコンセプトはまだ適用されます。EnableRewind()`がリクエストストリームを読み込んだ後に開発者が再読み込みできるように、一度書き込んだレスポンスストリームを読み込める.Net Coreの組み込み機能については、私は知らないのです。
あなたのアプローチは、おそらくまったく適切なものです。あなたは基本的に、シークできないストリームをシークできるストリームに変換しているのです。 最終的には Response.Body
ストリームを、バッファリングされシークをサポートするストリームと交換する必要があります。 このミドルウェアは、あなたのアプローチと非常によく似ていることにお気づきでしょう。 しかし、元のストリームを Response.Body
に戻すための追加保護として finally ブロックを使用し、構文が少し単純なので Seek
メソッドではなくストリームの Position
プロパティを使用しましたが、効果はあなたのアプローチと変わりません。
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ミドルウェア基礎編】(https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware)を参考にしました。
警告
next
を実行した後にHttpResponse
を変更する際には注意が必要です。 レスポンスはすでにクライアントに送信されている可能性があります。次のような使い方ができます。 HttpResponse.HasStarted で、ヘッダが送信されたかどうかを確認することができます。警告
write
メソッドを呼び出した後にnext.Invoke
を呼び出さないようにしてください。ミドルウェア コンポーネントは、レスポンスを生成するか、next.Invoke
を呼び出すかのどちらかですが、これはできません。 その両方。
基本ミドルウェアの組み込み例 aspnet/BasicMiddleware Github repoより引用
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);
}
}
}
ミドルウェア](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1)を使用することができます。 をリクエストパイプラインで使用すると、リクエストとレスポンスのログを取ることができます。
ただし、以下の理由により、「メモリリーク」の危険性が増します。 1.ストリーム 2.バイトバッファの設定と 3.文字列の変換
は、Large Object Heap(リクエストやレスポンスのボディが85,000バイト以上の場合)に達する可能性があります。これは、アプリケーションにおけるメモリーリークの危険性を高めます。 LOH を回避するために、関連する ライブラリ を使って、メモリストリームを 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;
}
}
注)。LOHの危険性は textWriter.ToString()
のために完全に払拭されていませんが、一方で構造化ロギングをサポートするロギングクライアントライブラリ(例えばSerilog)を使用して、Recyclable Memory Streamのインスタンスをインジェクトすることができます。