Reputation: 133
I have a dotnetcore 3.1 API project that receives HTTP requests, passes pertinent data to a service layer, which then does business logic and writes the data to the database. Pretty standard stuff. Additionally, I have a custom Middleware class that uses a Stopwatch to profile the execution time of incoming requests, and logs the URI, timestamp, elapsed time, request headers / body, response status code, and response headers / body to my database for profiling and debugging purposes.
Everything works correctly when I stand up the API in IIS and use Postman to POST a request with a given JSON body; the Middleware logs the request, and the data is written to the DB as expected. However, only while running my integration test suite using the in-memory TestServer from .net, the POST request fails due to an empty request body (it is an empty string, not even an empty JSON object).
I suspected this was due to somehow incorrectly resetting the request stream after the Middleware reads it, but the baffling thing is the HttpContext.Request is already empty before the Middleware runs. When stepping through the code, I have confirmed that the HttpRequestMessage Content is set correctly, that the JSON is correct, and that the test passes when the Middleware is not being used, so the test logic is not the issue as far as I can tell. Setting a breakpoint in the Middleware InvokeAsync and inspecting the value of pHttpContext shows that the content is already null.
Here's a simplified version of the code for my controller along with the code for the integration test method and the middleware. Any assistance would be appreciated.
Controller Method:
[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();
}
Test Method:
[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);
}
Middleware:
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;
}
}
Upvotes: 0
Views: 2896
Reputation: 133
I ended up going back to the drawing board on the logging middleware. I implemented a more straightforward solution based on this Jeremy Meng blog post. This code works both when deployed and in my integration tests.
New middleware code:
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);
}
}
}
Upvotes: 3