I am looking for feedbacks about my ASP.NET Core Request / Response logging middleware, in particular in ways to lower the memory footprint and string allocations:
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestResponseLoggingMiddleware> _logger;
private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager;
private const int ReadChunkBufferLength = 4096;
public RequestResponseLoggingMiddleware(RequestDelegate next, ILogger<RequestResponseLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
_recyclableMemoryStreamManager = new RecyclableMemoryStreamManager();
}
public async Task Invoke(HttpContext context)
{
var requestProfilerModel = new RequestProfilerModel
{
RequestTime = new DateTimeOffset(),
Context = context,
Request = await FormatRequestAsync(context)
};
var originalBody = context.Response.Body;
using (var newResponseBody = _recyclableMemoryStreamManager.GetStream())
{
context.Response.Body = newResponseBody;
await _next(context);
newResponseBody.Seek(0, SeekOrigin.Begin);
await newResponseBody.CopyToAsync(originalBody);
newResponseBody.Seek(0, SeekOrigin.Begin);
requestProfilerModel.Response = await FormatResponseAsync(context, newResponseBody);
requestProfilerModel.ResponseTime = new DateTimeOffset();
_logger.LogInformation(requestProfilerModel.Request);
_logger.LogInformation(requestProfilerModel.Response);
}
}
private async Task<string> FormatResponseAsync(HttpContext context, Stream newResponseBody)
{
var request = context.Request;
var response = context.Response;
return $"Http Response Information: {Environment.NewLine}" +
$"Schema: {request.Scheme} {Environment.NewLine}" +
$"Host: {request.Host} {Environment.NewLine}" +
$"Path: {request.Path} {Environment.NewLine}" +
$"QueryString: {request.QueryString} {Environment.NewLine}" +
$"Headers: {Environment.NewLine}" + FormatHeaders(response.Headers) +
$"StatusCode: {response.StatusCode} {Environment.NewLine}" +
$"Response Body: {await ReadStreamInChunksAsync(newResponseBody)}";
}
private async Task<string> FormatRequestAsync(HttpContext context)
{
var request = context.Request;
return $"Http Request Information: {Environment.NewLine}" +
$"Schema:{request.Scheme} {Environment.NewLine}" +
$"Host: {request.Host} {Environment.NewLine}" +
$"Path: {request.Path} {Environment.NewLine}" +
$"QueryString: {request.QueryString} {Environment.NewLine}" +
$"Headers: {Environment.NewLine}" + FormatHeaders(request.Headers) +
$"Request Body: {await GetRequestBodyAsync(request)}";
}
private string FormatHeaders(IHeaderDictionary headers)
{
var stringBuilder = new StringBuilder();
foreach (var (key, value) in headers)
{
stringBuilder.AppendLine($"- {key}: {value}");
}
return stringBuilder.ToString();
}
public async Task<string> GetRequestBodyAsync(HttpRequest request)
{
request.EnableBuffering();
request.EnableRewind();
using (var stream = _recyclableMemoryStreamManager.GetStream())
{
await request.Body.CopyToAsync(stream);
request.Body.Seek(0, SeekOrigin.Begin);
return await ReadStreamInChunksAsync(stream);
}
}
private static async Task<string> ReadStreamInChunksAsync(Stream stream)
{
stream.Seek(0, SeekOrigin.Begin);
string result;
using (var stringWriter = new StringWriter())
using (var streamReader = new StreamReader(stream))
{
var readChunk = new char[ReadChunkBufferLength];
int readChunkLength;
//do while: is useful for the last iteration in case readChunkLength < chunkLength
do
{
readChunkLength = await streamReader.ReadBlockAsync(readChunk, 0, ReadChunkBufferLength);
await stringWriter.WriteAsync(readChunk, 0, readChunkLength);
} while (readChunkLength > 0);
result = stringWriter.ToString();
}
return result;
}
}