2
\$\begingroup\$

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;
    }
}
\$\endgroup\$
1
\$\begingroup\$

I would recommend using Factory-based middleware instead of the by-convention method that you share here. You would then be able to have scoped DI.

For string concatenation, consider using a StringBuilder instead of the plus-operator everywhere. I can see you use it in some places, but that string concatenation with interpolation "looks" costly. Refer to the remarks section to see if the StringBuilder is a good choice for your use-case.

Experiment with ReadChunkBufferLength and consider making it a configurable value with a sane default. This SO post might provide some insight.

\$\endgroup\$
  • 1
    \$\begingroup\$ String concatenation with + only matters in loops. Here it doesn't make any difference and OP is already using StringBuilder with a loop. What's ugly are the new-lines. string.Join would be an option too. \$\endgroup\$ – t3chb0t Apr 29 at 6:39
  • \$\begingroup\$ @t3chb0t btw, nothing can be improved through Span<T>? \$\endgroup\$ – Ehouarn Perret Apr 29 at 8:20
  • \$\begingroup\$ @EhouarnPerret Span<T> is for reading and creating string views to avoid copying it, you are only creating strings and writing so I don't thing it could help. \$\endgroup\$ – t3chb0t Apr 29 at 8:22
  • \$\begingroup\$ @t3chb0t okay makes sense, thanks for the clarifications! \$\endgroup\$ – Ehouarn Perret Apr 29 at 9:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.