usestaticfiles services net injection iconfiguration dependency configureservices asp c# asp.net-core asp.net-core-mvc asp.net-core-middleware

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 a next , 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 de write . Un componente de middleware produce una respuesta o llama a next.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.