深度优先

这个家伙好懒,除了文章什么都没留下

0%

【.Net Core】获取请求和响应信息

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)
{
//code dealing with the request

await _next(context);

//code dealing with the response
}

接下来,添加一个静态类以简化将中间件添加到应用程序管道中的过程。这与内置中间件使用的模式相同。

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);
};
}
}
}