The Problem
In .NET 4.x it was straightforward to read the request body of an incoming HTTP request, log the information and also log the outgoing response back to the caller. I thought it would be just as easy in .Net Core, how wrong a I was as Microsoft do their utmost to hide the Request body from you.
The Solution
Accessing the Request and Response bodies must be done in HTTP Middleware, and, after reading much on StackOverflow I came up with the following solution (and my thanks to all the original authors and people who helped me). I cannot take credit for this code, I just adapted it from ideas on StackOverflow
Remember
For .Net Core 2.1 use context.HttpContext.Request.EnableRewind();
For .Net Core 3.1 context.HttpContext.Request.EnableBuffering();
/// <summary> /// Want to be able to read the Request.Body to get the incoming JSON /// In .net 4 this was more or less simple. In Core it has changed /// Need to be able to "rewind" of Request.Body - have to do that /// Thanks to /// https://stackoverflow.com/questions/40494913/how-to-read-request-body-in-a-asp-net-core-webapi-controller /// in request pipeline middleware /// /// For ASP.NET 2.1 /// context.HttpContext.Request.EnableRewind(); /// For ASP.NET 3.1 /// context.HttpContext.Request.EnableBuffering(); /// /// See also https://stackoverflow.com/questions/43403941/how-to-read-asp-net-core-response-body/43404745 /// </summary> public class LogRequestResponseMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly RecyclableMemoryStreamManager _recyclableMemoryStreamManager; private const int ReadChunkBufferLength = 4096; private bool LogRequest; private bool logResponse; public LogRequestResponseMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, bool requestLogging, bool responseLogging) { //-------------------------------------------------------------------- // Config | //-------------------------------------------------------------------- this.LogRequest = requestLogging; this.logResponse = responseLogging; _next = next; _logger = loggerFactory.CreateLogger<LogRequestResponseMiddleware>(); //-------------------------------------------------------------------- // Create stream to use in place of HttpContext.Response.Body stream | // which is write-only | //-------------------------------------------------------------------- _recyclableMemoryStreamManager = new RecyclableMemoryStreamManager(); } public virtual async Task Invoke(HttpContext context) { //----------------------------------------------------- // Request | //----------------------------------------------------- if (this.LogRequest) { //------------------------------------------------- // Turn on Buffering/Rewind | //------------------------------------------------- context.Request.EnableBuffering(); //----------------------------------------------------- // Log Request | //----------------------------------------------------- await this.LogRequestAsync(context); } //----------------------------------------------------- // Response | //----------------------------------------------------- if (this.logResponse) { //----------------------------------------------------- // Log Response | //----------------------------------------------------- await this.LogResponseAsync(context); } else { //----------------------------------------------------- // Fire next in chain | //----------------------------------------------------- await _next.Invoke(context); } } /// <summary> /// Log the incoming request /// </summary> /// <param name="context"></param> /// <returns></returns> private async Task LogRequestAsync(HttpContext context) { using (var requestStream = _recyclableMemoryStreamManager.GetStream()) { var origPosition = context.Request.Body.Position; context.Request.Body.Position = 0; await context.Request.Body.CopyToAsync(requestStream); context.Request.Body.Position = origPosition; _logger.LogInformation($"Http Request Information:{Environment.NewLine}" + $"Schema:{context.Request.Scheme} " + $"Host: {context.Request.Host} " + $"Path: {context.Request.Path} " + $"QueryString: {context.Request.QueryString} " + $"Request Body: {ReadStreamInChunks(requestStream)}"); } } private async Task LogResponseAsync(HttpContext context) { //-------------------------------------------------------- // Keep reference to original body | //-------------------------------------------------------- var originalBody = context.Response.Body; using (var responseStream = _recyclableMemoryStreamManager.GetStream()) { //----------------------------------------------------- // Replace existing with read/write stream | //----------------------------------------------------- context.Response.Body = responseStream; //----------------------------------------------------- // Fire next in chain | //----------------------------------------------------- await _next.Invoke(context); //----------------------------------------------------- // Get Result Back | //----------------------------------------------------- string resp = ReadStreamInChunks(responseStream); //------------------------------------------------------ // Write it back to original Response Body | //------------------------------------------------------ byte[] response = Encoding.ASCII.GetBytes(resp); await originalBody.WriteAsync(response, 0, response.Length); //------------------------------------------------------ // Log It | //------------------------------------------------------ _logger.LogInformation($"Http Response Information:{Environment.NewLine}" + $"Schema:{context.Request.Scheme} " + $"Host: {context.Request.Host} " + $"Path: {context.Request.Path} " + $"QueryString: {context.Request.QueryString} " + $"Response Body: {resp}"); } //----------------------------------------------------------- // Restore | //----------------------------------------------------------- context.Response.Body = originalBody; } private static string ReadStreamInChunks(Stream stream) { stream.Seek(0, SeekOrigin.Begin); string result; using (var textWriter = new StringWriter()) using (var reader = new StreamReader(stream)) { var readChunk = new char[ReadChunkBufferLength]; int readChunkLength; //do while: is useful for the last iteration in case readChunkLength < chunkLength do { readChunkLength = reader.ReadBlock(readChunk, 0, ReadChunkBufferLength); textWriter.Write(readChunk, 0, readChunkLength); } while (readChunkLength > 0); result = textWriter.ToString(); } return result; } }