Dotnetcore 3.1 中间件 HttpContext.Request 在 in-memory TestServer 集成测试期间运行中间件之前已经为空

Dotnetcore 3.1 middleware HttpContext.Request is already null before middleware runs during in-memory TestServer integration test

我有一个 dotnetcore 3.1 API 项目,它接收 HTTP 请求,将相关数据传递到服务层,然后服务层执行业务逻辑并将数据写入数据库。很标准的东西。此外,我有一个自定义中间件 class,它使用秒表来分析传入请求的执行时间,并记录 URI、时间戳、运行时间、请求 headers / body、响应状态代码和响应 headers / body 到我的数据库以进行分析和调试。

当我在 IIS 中启动 API 并使用 Postman POST 给定 JSON body 的请求时,一切正常;中间件记录请求,数据按预期写入数据库。但是,,而运行 我的集成测试套件使用来自.net 的in-memory TestServer,POST 请求因空请求而失败body(它是一个空字符串,甚至不是一个空的 JSON object)。

我怀疑这是由于在中间件读取后以某种方式错误地重置了请求流,但令人困惑的是 HttpContext.Request 在中间件运行之前 已经为空 .单步执行代码时,我已确认 HttpRequestMessage 内容设置正确,JSON 正确,并且在未使用中间件时测试通过,因此测试逻辑目前不是问题据我所知。在中间件 InvokeAsync 中设置断点并检查 pHttpContext 的值显示内容已为空。

这是我的控制器代码的简化版本以及集成测试方法和中间件的代码。如有任何帮助,我们将不胜感激。


控制器方法:

[HttpPost]
public IActionResult Create([FromBody] Widget pWidget)
{
    _WidgetService.CreateWidget(new CreateWidget()
    {
        WidgetNo = pWidget.WidgetNo,
        StatusId = pWidget.StatusId,
        WidgetTypeId = pWidget.WidgetTypeId,
        CreatedBy = pWidget.CreatedBy
    });
    return Ok();
}

测试方法:

[Theory]
[InlineData("/api/v1/widget")]
public async Task PostCreatesWidget(String pUrl)
{
    // Arrange
    List<SelectWidget> originalWidgets = _widgetService.GetAllWidgets().ToList();
    Widget createdWidget = _widgetGenerator.GenerateModel();
    String json = JsonConvert.SerializeObject(createdWidget);
    Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    HttpRequestMessage request = new HttpRequestMessage()
    {
        RequestUri = new Uri(pUrl, UriKind.Relative),
        Content = new StringContent(json, Encoding.UTF8, "application/json"),
        Method = HttpMethod.Post,
    };

    // Act
    HttpResponseMessage response = await Client.SendAsync(request);
    String responseContent = await response.Content.ReadAsStringAsync();

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    List<SelectWidget> newWidgets = _widgetService.GetAllWidgets().ToList();
    Assert.True(newWidgets.Count == originalWidgets.Count + 1);
}

中间件:

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IRequestLoggingService _requestLoggingService;

    public RequestLoggingMiddleware(RequestDelegate pNext, IRequestLoggingService pRequestLoggingService)
    {
        _next = pNext;
        _requestLoggingService = pRequestLoggingService;
    }

    public async Task InvokeAsync(HttpContext pHttpContext)
    {
        try
        {
            HttpRequest request = pHttpContext.Request;

            if (request.Path.StartsWithSegments(new PathString("/api")))
            {
                Stopwatch stopwatch = Stopwatch.StartNew();
                DateTime requestTime = DateTime.UtcNow;
                String requestBodyContent = await ReadRequestBody(request);
                Stream bodyStream = pHttpContext.Response.Body;

                using (MemoryStream responseBody = new MemoryStream())
                {
                    HttpResponse response = pHttpContext.Response;
                    response.Body = responseBody;
                    await _next(pHttpContext);
                    stopwatch.Stop();

                    String responseBodyContent = null;
                    responseBodyContent = await ReadResponseBody(response);
                    await responseBody.CopyToAsync(bodyStream);

                    await _requestLoggingService.LogRequest(new InsertRequestLog()
                    {
                        DateRequested = requestTime,
                        ResponseDuration = stopwatch.ElapsedMilliseconds,
                        StatusCode = response.StatusCode,
                        Method = request.Method,
                        Path = request.Path,
                        QueryString = request.QueryString.ToString(),
                        RequestBody = requestBodyContent,
                        ResponseBody = responseBodyContent
                    });
                }
            }
            else
            {
                await _next(pHttpContext);
            }
        }
        catch (Exception)
        {
            await _next(pHttpContext);
        }
    }

    private async Task<String> ReadRequestBody(HttpRequest pRequest)
    {
        pRequest.EnableBuffering();

        Byte[] buffer = new Byte[Convert.ToInt32(pRequest.ContentLength)];
        await pRequest.Body.ReadAsync(buffer, 0, buffer.Length);
        String bodyAsText = Encoding.UTF8.GetString(buffer);
        pRequest.Body.Seek(0, SeekOrigin.Begin);

        return bodyAsText;
    }

    private async Task<String> ReadResponseBody(HttpResponse pResponse)
    {
        pResponse.Body.Seek(0, SeekOrigin.Begin);
        String bodyAsText = await new StreamReader(pResponse.Body).ReadToEndAsync();
        pResponse.Body.Seek(0, SeekOrigin.Begin);

        return bodyAsText;
    }
}

我最终回到了日志记录中间件的绘图板上。我基于这个 Jeremy Meng blog post 实现了一个更直接的解决方案。此代码在部署时和我的集成测试中都有效。

新的中间件代码:

public class RequestLoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IRequestLoggingService _requestLoggingService;

    public RequestLoggingMiddleware(RequestDelegate pNext, IRequestLoggingService pRequestLoggingService)
    {
        _next = pNext;
        _requestLoggingService = pRequestLoggingService;
    }

    public async Task InvokeAsync(HttpContext pHttpContext)
    {
        if (pHttpContext.Request.Path.StartsWithSegments(new PathString("/api")))
        {
            String requestBody;
            String responseBody = "";
            DateTime requestTime = DateTime.UtcNow;
            Stopwatch stopwatch;

            pHttpContext.Request.EnableBuffering();

            using (StreamReader reader = new StreamReader(pHttpContext.Request.Body,
                                                          encoding: Encoding.UTF8,
                                                          detectEncodingFromByteOrderMarks: false,
                                                          leaveOpen: true))
            {
                requestBody = await reader.ReadToEndAsync();
                pHttpContext.Request.Body.Position = 0;
            }

            Stream originalResponseStream = pHttpContext.Response.Body;
            using (MemoryStream responseStream = new MemoryStream())
            {
                pHttpContext.Response.Body = responseStream;

                stopwatch = Stopwatch.StartNew();
                await _next(pHttpContext);
                stopwatch.Stop();

                pHttpContext.Response.Body.Seek(0, SeekOrigin.Begin);
                responseBody = await new StreamReader(pHttpContext.Response.Body).ReadToEndAsync();
                pHttpContext.Response.Body.Seek(0, SeekOrigin.Begin);

                await responseStream.CopyToAsync(originalResponseStream);
            }

            await _requestLoggingService.LogRequest(new InsertRequestLog()
            {
                DateRequested = requestTime,
                ResponseDuration = stopwatch.ElapsedMilliseconds,
                StatusCode = pHttpContext.Response.StatusCode,
                Method = pHttpContext.Request.Method,
                Path = pHttpContext.Request.Path,
                QueryString = pHttpContext.Request.QueryString.ToString(),
                RequestBody = requestBody,
                ResponseBody = responseBody
            });
        }
        else
        {
            await _next(pHttpContext);
        }
    }
}