using System.Buffers;
using System.IO.Pipelines;
using Wangkanai.Detection.Services;
namespace Dpz.Core.Web.Library.Middleware;
public class HttpRequestRecord(RequestDelegate next, IDiagnosticContext diagnosticContext)
{
public async Task InvokeAsync(
HttpContext httpContext,
IArticleService articleService,
IConfiguration configuration,
IDetectionService detectionService
)
{
// 处理 sitemap 文件重定向(支持 sitemap.xml, sitemap_1.xml, sitemap_2.xml 等)
var path = httpContext.Request.Path.Value ?? "";
if (IsSitemapRequest(path) && string.Equals(configuration["AgileConfig:env"], "PROD"))
{
var host = configuration.GetSection("upyun")["Host"] ?? "https://localhost:37701";
var baseUri = new Uri(host);
var fileName = Path.GetFileName(path);
var uri = new Uri(baseUri, $"/sitemap/{fileName}");
httpContext.Response.Redirect(uri.ToString());
return;
}
SetDetection(detectionService);
await EnrichFromRequestAsync(httpContext.Request);
await next.Invoke(httpContext);
var routeValues = httpContext.Request.RouteValues;
if (
routeValues["controller"] is "Article"
&& routeValues["action"] is "Read"
&& routeValues["id"] is string id
)
{
await articleService.ViewAsync(id);
}
}
private async Task EnrichFromRequestAsync(HttpRequest request)
{
if (request.Query is { Count: > 0 })
{
diagnosticContext.Set("QueryString", request.Query);
}
var bodyContent = await GetBodyContentAsync(request.BodyReader);
if (bodyContent != string.Empty)
{
diagnosticContext.Set("RequestBody", bodyContent);
}
}
/// <summary>
/// 读取 http body
/// 如果 body 超过 1 << 10 字节,则不记录 body
/// </summary>
/// <param name="reader"></param>
/// <returns></returns>
private static async Task<string> GetBodyContentAsync(PipeReader reader)
{
var result = new StringBuilder();
const long maxLength = 1 << 10;
long totalLength = 0;
while (true)
{
var readResult = await reader.ReadAsync();
var buffer = readResult.Buffer;
// 如果 buffer 长度已经大于等于 maxLength,直接截取并返回
if (buffer.Length >= maxLength)
{
var span = buffer.Slice(0, maxLength);
AppendString(result, span);
result.Append("...");
reader.AdvanceTo(buffer.Start, buffer.GetPosition(maxLength));
//await reader.CompleteAsync();
break;
}
totalLength += buffer.Length;
if (readResult.IsCompleted && buffer.Length > 0)
{
AppendString(result, in buffer);
}
reader.AdvanceTo(buffer.Start, buffer.End);
if (totalLength >= maxLength)
{
result.Append("...");
break;
}
if (readResult.IsCompleted)
{
break;
}
}
return result.ToString();
}
private static void AppendString(
StringBuilder result,
in ReadOnlySequence<byte> readOnlySequence
)
{
var span = readOnlySequence.IsSingleSegment
? readOnlySequence.First.Span
: readOnlySequence.ToArray().AsSpan();
result.Append(Encoding.UTF8.GetString(span));
}
private void SetDetection(IDetectionService detectionService)
{
diagnosticContext.Set("Device", detectionService.Device.Type.ToString());
diagnosticContext.Set("Browser", detectionService.Browser.Name.ToString());
diagnosticContext.Set("BrowserVersion", detectionService.Browser.Version.ToString());
diagnosticContext.Set("Platform", detectionService.Platform.Name.ToString());
diagnosticContext.Set("Engine", detectionService.Engine.Name.ToString());
diagnosticContext.Set("Crawler", detectionService.Crawler.Name.ToString());
diagnosticContext.Set("UserAgent", detectionService.UserAgent.ToString());
}
/// <summary>
/// 判断是否为 sitemap 请求
/// 匹配: /sitemap.xml, /sitemap_1.xml, /sitemap_2.xml 等
/// </summary>
private static bool IsSitemapRequest(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
var fileName = Path.GetFileName(path);
return fileName.Equals("sitemap.xml", StringComparison.OrdinalIgnoreCase)
|| (
fileName.StartsWith("sitemap_", StringComparison.OrdinalIgnoreCase)
&& fileName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)
);
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
下面分段解释这段中间件(HttpRequestRecord)的功能、实现细节和需要注意的地方。
总体功能概览
- 这是一个 ASP.NET Core 中间件(通过构造函数注入 RequestDelegate 和 IDiagnosticContext)。
- 主要职责有三点:
- 在生产环境下对 sitemap 请求(如 /sitemap.xml、/sitemap_1.xml 等)做外部重定向到配置的 upyun 主机。
- 收集并把一些请求信息(QueryString、请求 Body)和客户端探测信息(设备、浏览器、平台等)写入 diagnosticContext,以便后续日志/诊断使用。
- 在请求处理完成后,如果路由为 Article/Read 且有 id 字符串,则调用 articleService.ViewAsync(id)(通常用于记录文章被访问/增加阅读量)。
InvokeAsync 的执行流程
处理 sitemap 重定向:
- 通过 IsSitemapRequest 判断请求路径是否匹配 sitemap 模式。
- 仅当 configuration["AgileConfig:env"] 精确等于 "PROD" 时才重定向。
- 从配置节 "upyun" 获取 Host(若没有则使用默认),拼接 /sitemap/{fileName} 并调用 httpContext.Response.Redirect。
- 重定向后直接返回,不继续后续中间件。
设置探测信息(SetDetection):
- 使用注入的 IDetectionService,把 Device、Browser、BrowserVersion、Platform、Engine、Crawler、UserAgent 等信息写入 diagnosticContext。
收集请求信息(EnrichFromRequestAsync):
- 如果请求 Query 有键值,则把 request.Query 作为 "QueryString" 放入 diagnosticContext。
- 读取请求 Body(通过 HttpRequest.BodyReader)并限制长度(见下文 GetBodyContentAsync),如果非空则写入 diagnosticContext 的 "RequestBody"。
调用下一个中间件:
- await next.Invoke(httpContext);
后处理(在下游处理完成后):
- 检查 httpContext.Request.RouteValues,若 controller == "Article" && action == "Read" && id 是 string,则调用 articleService.ViewAsync(id)。
GetBodyContentAsync 的实现细节
- 使用 PipeReader(ReadAsync)逐块读取请求体(ReadOnlySequence
)。 - 最大记录长度为 maxLength = 1 << 10(1024 字节)。如果读取到的缓冲区长度达到或超过该值,会截取前 1024 字节并在末尾追加 "...",然后停止读取。
- 在每次读取后通过 reader.AdvanceTo(...) 告知管线已消费的区域,避免阻塞。
- 最终返回按 UTF-8 解码的字符串(通过 AppendString 转换 ReadOnlySequence
)。 - AppendString 会处理单段和多段序列:单段直接取 Span,若多段则 ToArray 然后 AsSpan,再 UTF-8 解码。
IsSitemapRequest 的判定规则
- 取路径最后的文件名:
- 等于 "sitemap.xml"(不区分大小写),或
- 以 "sitemap_" 开头并以 ".xml" 结尾(不区分大小写)。
- 空或 null 路径返回 false。
SetDetection 的作用
- 从 IDetectionService 读取各种探测信息并通过 diagnosticContext.Set 写入相应键,方便日志系统或诊断系统捕获这些上下文信息。
注意点与潜在问题(建议)
- 读取请求体会消费请求流:
- 直接读取 HttpRequest.BodyReader 的数据会导致后续中间件/控制器无法再读取请求体(除非做了缓冲/重置)。通常需要在读取前调用 HttpRequest.EnableBuffering(),读取后将流位置重置(例如 request.Body.Position = 0 或 Seek(0, Begin)),或者使用能与管线配合的缓冲方法以保证下游可再次读取。
- Env 比较是大小写敏感的:configuration["AgileConfig:env"] == "PROD"。若配置写成 prod 或 Prod 会不匹配,建议使用不区分大小写的比较。
- AdvanceTo 的使用需要谨慎,确保不会因未正确推进而导致死循环或内存增长。当前实现基本合理,但要确保 reader.AdvanceTo 的端点正确选择。
- 使用 ReadOnlySequence.ToArray() 在多段情况下会分配新数组,可能在大请求时导致性能/内存开销,不过你已经限制了最大读取 1KB,影响有限。
- routeValues 的模式匹配使用 is 字面量做比较,语义上正确,但注意如果路由值不是字符串或键不存在会使条件不成立(这是预期行为)。
简要结论
- 这是一个用于日志/诊断增强的中间件,提供 sitemap 重定向(生产环境)、客户端探测信息记录、查询字符串与请求体(最多 1KB)记录,并在文章阅读路由结束后触发统计(articleService.ViewAsync)。在使用时应注意请求体读取对下游管线的影响,并考虑环境判断大小写与配置健壮性。
评论加载中...