Google 机器翻译: https://elanderson.net/2019/12/log-requests-and-responses-in-asp-net-core-3/
这篇文章将是 对ASP.NET Core 中的 日志请求和响应 的更新,该 文章不再适用于更现代的ASP.NET Core版本。 在大多数情况下,该帖子将与原始帖子完全匹配,但代码位已更新。
作为尝试进行调试的一部分,我需要一种记录请求和响应的方法。 编写中间件似乎是解决此问题的一种好方法。 结果也比我预期的处理请求和响应机构要复杂得多。
中间件 在ASP.NET Core中, 中间件 是组成HTTP管道的组件,该HTTP管道处理应用程序的请求和响应。 每个被调用的中间件都可以选择对请求进行一些处理,然后再在线调用下一个中间件。 在执行从调用返回到下一个中间件之后,就有机会对响应进行处理。
应用程序的HTTP管道是在Startup 类 的 Configure 函数中 设置的 。Run ,Map 和Use 是可用的三种中间件类型。Run 只能用于终止管道。Map 用于管道分支。Use 似乎是最常见的中间件类型,它进行一些处理并直接调用下一个中间件。 有关更多详细信息,请参见官方 文档 。
创建中间件 中间件可以直接在 Configure 函数中 实现为lambda ,但更典型的是,它可以实现为使用 IApplicationBuilder 上的扩展方法添加到管道中的类 。 本示例将使用类路由。
本示例是一个中间件,它使用ASP.NET Core的内置日志记录请求和响应。 创建一个名为 LoggerMiddleware 的类 。
该类将需要一个带有两个参数的构造函数,这两个参数都将由ASP.NET Core的依赖项注入系统提供。 第一个是 RequestDelegate,它将成为 管道中的下一个中间件。 第二个是 ILoggerFactory 的实例,该实例 将用于创建记录器。 所述 RequestDelegate 存储到类级别 _next 变量和 的LoggerFactory 用于创建存储到类电平的记录器 _logger 变量。
1 2 3 4 5 6 7 8 9 10 11 12 public class LoggerMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; public LoggerMiddleware (RequestDelegate next, ILoggerFactory loggerFactory ) { _next = next; _logger = loggerFactory .CreateLogger<LoggerMiddleware>(); } }
添加一个Invoke函数,当管道运行中间件时将调用该函数。下面的函数除了调用管道中的下一个中间件外什么也不做。
1 2 3 4 5 6 7 8 public async Task Invoke (HttpContext context ){ await _next(context); }
接下来,添加一个静态类以简化将中间件添加到应用程序管道中的过程。这与内置中间件使用的模式相同。
1 2 3 4 5 6 7 public static class LoggerMiddlewareExtensions { public static IApplicationBuilder UseRequestResponseLogging (this IApplicationBuilder builder ) { return builder.UseMiddleware<LoggerMiddleware>(); } }
添加到管道 要将新的中间件添加到管道中,请打开 Startup.cs 文件,并将以下行添加 到 Configure 函数中。
1 app.UseRequestResponseLogging();
请记住,中间件的添加顺序可能会影响应用程序的行为。 由于该帖子正在处理的中间件是日志记录,因此我将其放置在管道的开始位置附近。
记录请求和响应 现在我们新的中间件的设置工作已经完成,我们将回到其 调用 功能。 如前所述,这最终比我预期的要复杂得多,但是值得庆幸的是,我发现 Sul Aga 做到了 这一点 , 这确实帮助我解决了我所遇到的问题,并对这篇文章的原始版本提出了很多反馈。
有关此帖子原始版本的反馈之一是关于 潜在的内存泄漏和使用可回收内存流 。 首先,添加对 Microsoft.IO.RecyclableMemoryStream 包 的NuGet引用 。 接下来,我们将添加一个类级变量来保存一个 RecyclableMemoryStreamManager 的实例,该实例 将在构造函数中创建。 以下是更新的类视图,其中包含这些更改以及对 Invoke 函数和日志记录方法的存根的 更改 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class LoggerMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; public LoggerMiddleware (RequestDelegate next, ILoggerFactory loggerFactory ) { _next = next; _logger = loggerFactory .CreateLogger<LoggerMiddleware>(); _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); } public async Task Invoke (HttpContext context ) { await LogRequest(context); await LogResponse(context); } private async Task LogRequest (HttpContext context ) {} private async Task LogResponse (HttpContext context ) {} }
首先,我们将看一下LogRequest函数及其使用的辅助函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 private async Task LogRequest (HttpContext context ){ context.Request.EnableBuffering(); await using var requestStream = _recyclableMemoryStreamManager.GetStream(); await context.Request.Body.CopyToAsync(requestStream); _logger.LogInformation($"Http Request Information:{Environment.NewLine} " + $"Schema:{context.Request.Scheme} " + $"Host: {context.Request.Host} " + $"Path: {context.Request.Path} " + $"QueryString: {context.Request.QueryString} " + $"Request Body: {ReadStreamInChunks(requestStream)} " ); context.Request.Body.Position = 0 ; } private static string ReadStreamInChunks (Stream stream ){ const int readChunkBufferLength = 4096 ; stream.Seek(0 , SeekOrigin.Begin); using var textWriter = new StringWriter(); using var reader = new StreamReader(stream); var readChunk = new char [readChunkBufferLength]; int readChunkLength; do { readChunkLength = reader.ReadBlock(readChunk, 0 , readChunkBufferLength); textWriter.Write(readChunk, 0 , readChunkLength); } while (readChunkLength > 0 ); return textWriter.ToString(); }
使此功能起作用并允许读取请求正文的关键是 context.Request.EnableBuffering() ,它使我们能够从流的开头进行读取。 该功能的其余部分非常简单。
下一个函数是 LogResponse ,它用于使用 _next(context)等待 管道中的下一个中间件 ,然后在其余管道运行后记录响应主体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private async Task LogResponse (HttpContext context ){ var originalBodyStream = context.Response.Body; await using var responseBody = _recyclableMemoryStreamManager.GetStream(); context.Response.Body = responseBody; await _next(context); context.Response.Body.Seek(0 , SeekOrigin.Begin); var text = await new StreamReader(context.Response.Body).ReadToEndAsync(); context.Response.Body.Seek(0 , SeekOrigin.Begin); _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: {text} " ); await responseBody.CopyToAsync(originalBodyStream); }
如您所见,读取响应主体的技巧是将正在使用的流替换为新的 MemoryStream ,然后将数据复制回原始主体流。 我不知道这会对性能有多大影响,因此请确保在生产环境中使用它之前先研究它如何扩展。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 using Microsoft.AspNetCore.Http;using Microsoft.Extensions.Logging;using Microsoft.IO;using System;using System.Collections.Generic;using System.IO;using System.Linq;using System.Threading.Tasks;namespace demo.Extensions { public class LoggerMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; public LoggerMiddleware (RequestDelegate next, ILoggerFactory loggerFactory ) { _next = next; _logger = loggerFactory.CreateLogger<LoggerMiddleware>(); _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); } public async Task Invoke (HttpContext context ) { await LogRequest(context); await LogResponse(context); } private async Task LogRequest (HttpContext context ) { context.Request.EnableBuffering(); using (var requestStream = _recyclableMemoryStreamManager.GetStream()) { await context.Request.Body.CopyToAsync(requestStream); if (context.Request.Path.Value.StartsWith("/api/" )) { _logger.LogError($"Http Request Information:{Environment.NewLine} " + $"Schema:{context.Request.Scheme} " + $"Host: {context.Request.Host} " + $"Path: {context.Request.Path} " + $"QueryString: {context.Request.QueryString} " + $"Request Body: {ReadStreamInChunks(requestStream)} " ); } context.Request.Body.Position = 0 ; }; } private static string ReadStreamInChunks (Stream stream ) { const int readChunkBufferLength = 4096 ; stream.Seek(0 , SeekOrigin.Begin); using (var textWriter = new StringWriter()) { using (var reader = new StreamReader(stream)) { var readChunk = new char [readChunkBufferLength]; int readChunkLength; do { readChunkLength = reader.ReadBlock(readChunk, 0 , readChunkBufferLength); textWriter.Write(readChunk, 0 , readChunkLength); } while (readChunkLength > 0 ); return textWriter.ToString(); }; }; } private async Task LogResponse (HttpContext context ) { var originalBodyStream = context.Response.Body; using (var responseBody = _recyclableMemoryStreamManager.GetStream()) { context.Response.Body = responseBody; await _next(context); context.Response.Body.Seek(0 , SeekOrigin.Begin); var text = await new StreamReader(context.Response.Body).ReadToEndAsync(); context.Response.Body.Seek(0 , SeekOrigin.Begin); if (context.Request.Path.Value.StartsWith("/api/" )) { _logger.LogError($"Http Response Information:{Environment.NewLine} " + $"Schema:{context.Request.Scheme} " + $"Host: {context.Request.Host} " + $"Path: {context.Request.Path} " + $"QueryString: {context.Request.QueryString} " + $"Response Body: {text} " ); } await responseBody.CopyToAsync(originalBodyStream); }; } } }