c# - services - ¿Cómo leer ASP.NET Core Response.Body?
services asp net core (3)
En mi respuesta original, había leído completamente mal la pregunta y pensé que el afiche preguntaba cómo leer la
Request.Body
Pero él había preguntado cómo leer la
Response.Body
.
Response.Body
.
Dejo mi respuesta original para preservar el historial, pero también la actualizo para mostrar cómo respondería la pregunta una vez que la lea correctamente.
Respuesta original
Si desea una transmisión en búfer que admita la lectura varias veces, debe configurar
context.Request.EnableRewind()
Lo ideal es hacer esto temprano en el middleware antes de que algo necesite leer el cuerpo.
Entonces, por ejemplo, podría colocar el siguiente código al comienzo del método
Configure
del archivo Startup.cs:
app.Use(async (context, next) => {
context.Request.EnableRewind();
await next();
});
Antes de habilitar Rebobinar la secuencia asociada con la
Request.Body
es una secuencia de solo reenvío que no admite buscar o leer la secuencia por segunda vez.
Esto se hizo para que la configuración predeterminada del manejo de solicitudes sea lo más liviana y eficiente posible.
Pero una vez que habilita el rebobinado, la transmisión se actualiza a una transmisión que admite buscar y leer varias veces.
Puede observar esta "actualización" estableciendo un punto de interrupción justo antes y justo después de la llamada a
EnableRewind
y observando las propiedades
Request.Body
.
Entonces, por ejemplo,
Request.Body.CanSeek
cambiará de
false
a
true
.
actualización
: a partir de ASP.NET Core 2.1
Request.EnableBuffering()
está disponible, que actualiza
Request.Body
a
FileBufferingReadStream
al igual que
Request.EnableRewind()
y desde
Request.EnableBuffering()
está en un espacio de nombres público en lugar de uno interno. debería preferirse a EnableRewind ().
(Gracias a @ArjanEinbu por señalar)
Luego, para leer la secuencia del cuerpo, por ejemplo, puede hacer esto:
string bodyContent = new StreamReader(Request.Body).ReadToEnd();
Sin embargo, no envuelva la creación de
StreamReader
en una declaración de uso o cerrará la secuencia del cuerpo subyacente al final del bloque de uso y el código más adelante en el ciclo de vida de la solicitud no podrá leer el cuerpo.
También para estar seguros, podría ser una buena idea seguir la línea de código anterior que lee el contenido del cuerpo con esta línea de código para restablecer la posición de transmisión del cuerpo a 0.
request.Body.Position = 0;
De esa manera, cualquier código posterior en el ciclo de vida de la solicitud encontrará la solicitud. El cuerpo está en un estado como el que aún no se ha leído.
Respuesta actualizada
Lo siento, originalmente leí mal tu pregunta.
Todavía se aplica el concepto de actualizar la secuencia asociada para que sea una secuencia protegida.
Sin embargo, debe hacerlo manualmente, no conozco ninguna funcionalidad incorporada de .Net Core que le permita leer la secuencia de respuesta una vez escrita de la manera en que
EnableRewind()
permite que un desarrollador
EnableRewind()
a leer la secuencia de solicitud después de que se haya leído.
Su enfoque "hacky" es probablemente totalmente apropiado.
Básicamente, está convirtiendo una transmisión que no puede buscar a una que sí puede.
Al final del día, la secuencia
Response.Body
tiene que intercambiarse con una secuencia que está almacenada en el búfer y admite la búsqueda.
Aquí hay otra versión del middleware para hacer eso, pero notará que es bastante similar a su enfoque.
Sin embargo, elegí usar un bloque finalmente como protección adicional para volver a colocar la secuencia original en
Response.Body
y utilicé la propiedad
Position
de la secuencia en lugar del método
Seek
, ya que la sintaxis es un poco más simple pero el efecto no es diferente que tu enfoque
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;
}
}
He estado luchando por obtener la propiedad
Response.Body
de una acción ASP.NET Core y la única solución que he podido identificar parece subóptima.
La solución requiere intercambiar
Response.Body
con
MemoryStream
mientras lee la secuencia en una variable de cadena y luego la intercambia antes de enviarla al cliente.
En los ejemplos a continuación, intento obtener el valor
Response.Body
en una clase de middleware personalizada.
Response.Body
es una propiedad de
conjunto
único en ASP.NET Core por alguna razón?
¿Me estoy perdiendo algo aquí o es un problema de supervisión / error / diseño?
¿Hay una mejor manera de leer
Response.Body
?
Solución actual (subóptima):
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;
}
}
}
Intento de solución con EnableRewind ():
esto solo funciona para
Request.Body
, no
Response.Body
.
Esto resulta en la lectura de una cadena vacía de
Response.Body
lugar del contenido real del cuerpo de la respuesta.
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;
}
}
Lo que usted describe como un hack es en realidad el enfoque sugerido de cómo administrar las secuencias de respuesta en el middleware personalizado.
Debido a la naturaleza de la tubería del diseño de middleware donde cada middleware desconoce el controlador anterior o siguiente en la tubería. No hay garantía de que el middleware actual sea el que escriba la respuesta a menos que se aferre al flujo de respuesta que se le dio antes de pasar un flujo que controla (el middleware actual) Este diseño se vio en OWIN y finalmente se incluyó en asp.net-core.
Una vez que comienza a escribir en la secuencia de respuesta, envía el cuerpo y los encabezados (la respuesta) al cliente. Si otro manejador en el proceso lo hace antes de que el manejador actual tenga la oportunidad de hacerlo, no podrá agregar nada a la respuesta una vez que ya se haya enviado.
Que tampoco se garantiza que sea el flujo de respuesta real si el middleware anterior en la tubería siguió la misma estrategia de pasar otro flujo por la línea.
Referencia a los fundamentos de ASP.NET Core Middleware
Advertencia
Tenga cuidado al modificar
HttpResponse
después de invocar anext
, porque la respuesta ya puede haber sido enviada al cliente. Puede usar HttpResponse.HasStarted para verificar si se han enviado los encabezados.Advertencia
No llame a continuación.
next.Invoke
después de llamar a un método dewrite
. Un componente de middleware produce una respuesta o llama anext.Invoke
.next.Invoke
, pero no ambos.
Ejemplo de construido en middlewares básicos de 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);
}
}
}
Puede usar un middleware en la canalización de solicitudes para registrar solicitudes y respuestas.
Sin embargo, aumenta el riesgo de
memory leak
de
memory leak
, debido a que: 1. Flujos, 2. Configuración de búferes de bytes y 3. Conversiones de cadenas
puede terminar en Montón de objetos grandes (en caso de que el cuerpo de la solicitud o respuesta sea mayor a 85,000 bytes). Esto aumenta el riesgo de pérdida de memoria en su aplicación. Para evitar LOH, los flujos de memoria se pueden reemplazar por un flujo de memoria reciclable utilizando la library correspondiente.
Una implementación que utiliza flujos de memoria reciclables:
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;
}
}
NÓTESE BIEN.
El peligro de LOH no se erradica por completo debido a
textWriter.ToString()
por otro lado, puede usar una biblioteca de cliente de registro que admita el registro estructurado (es decir, Serilog) e inyectar la instancia de un flujo de memoria reciclable.