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);
}
}
}
我有一个 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);
}
}
}