diff --git a/src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs b/src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs
new file mode 100644
index 00000000..ec471dd1
--- /dev/null
+++ b/src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddleware.cs
@@ -0,0 +1,136 @@
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using NLog.Common;
+using NLog.Web.LayoutRenderers;
+
+namespace NLog.Web
+{
+ ///
+ /// This class is to intercept the HTTP pipeline and to allow additional logging of the following
+ ///
+ /// POST request body
+ ///
+ /// Usage: app.UseMiddleware<NLogRequestPostBodyMiddleware>(); where app is an IApplicationBuilder
+ /// Register the NLogRequestPostBodyMiddlewareOption in the IoC so that the config gets passed to the constructor
+ ///
+ public class NLogRequestPostedBodyMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ private readonly NLogRequestPostedBodyMiddlewareOptions _options;
+
+ ///
+ /// Constructor that takes a configuration
+ ///
+ ///
+ ///
+ public NLogRequestPostedBodyMiddleware(RequestDelegate next, NLogRequestPostedBodyMiddlewareOptions options)
+ {
+ _next = next;
+ _options = options;
+ }
+
+ ///
+ /// This allows interception of the HTTP pipeline for logging purposes
+ ///
+ /// The HttpContext
+ ///
+ public async Task Invoke(HttpContext context)
+ {
+ if (ShouldCaptureRequestBody(context))
+ {
+ // This is required, otherwise reading the request will destructively read the request
+ context.Request.EnableBuffering();
+
+ // Save the POST request body in HttpContext.Items with a key of '__nlog-aspnet-request-posted-body'
+ var requestBody = await GetString(context.Request.Body).ConfigureAwait(false);
+
+ if (!string.IsNullOrEmpty(requestBody))
+ {
+ context.Items[AspNetRequestPostedBodyLayoutRenderer.NLogPostedRequestBodyKey] = requestBody;
+ }
+ }
+
+ // Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
+ await _next(context).ConfigureAwait(false);
+ }
+
+ private bool ShouldCaptureRequestBody(HttpContext context)
+ {
+ // Perform null checking
+ if (context == null)
+ {
+ InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext is null");
+ // Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
+ return false;
+ }
+
+ if (context.Request == null)
+ {
+ InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext.Request is null");
+ // Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
+ return false;
+ }
+
+ // Perform null checking
+ if (context.Request.Body == null)
+ {
+ InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext.Request.Body stream is null");
+ // Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
+ return false;
+ }
+
+ // If we cannot read the stream we cannot capture the body
+ if (!context.Request.Body.CanRead)
+ {
+ InternalLogger.Debug("NLogRequestPostedBodyMiddleware: HttpContext.Request.Body stream is non-readable");
+ // Execute the next class in the HTTP pipeline, this can be the next middleware or the actual handler
+ return false;
+ }
+
+ return (_options.ShouldCapture(context));
+ }
+
+ ///
+ /// Convert the stream to a String for logging.
+ /// If the stream is binary please do not utilize this middleware
+ /// Arguably, logging a byte array in a sensible format is simply not possible.
+ ///
+ ///
+ /// The contents of the Stream read fully from start to end as a String
+ private async Task GetString(Stream stream)
+ {
+ // Save away the original stream position
+ var originalPosition = stream.Position;
+
+ // This is required to reset the stream position to the beginning in order to properly read all of the stream.
+ stream.Position = 0;
+
+ string responseText = null;
+
+ // The last argument, leaveOpen, is set to true, so that the stream is not pre-maturely closed
+ // therefore preventing the next reader from reading the stream.
+ // The middle three arguments are from the configuration instance
+ // These default to UTF-8, true, and 1024.
+ using (var streamReader = new StreamReader(
+ stream,
+ Encoding.UTF8,
+ true,
+ 1024,
+ leaveOpen: true))
+ {
+ // This is the most straight forward logic to read the entire body
+ responseText = await streamReader.ReadToEndAsync().ConfigureAwait(false);
+ }
+
+ // This is required to reset the stream position to the original, in order to
+ // properly let the next reader process the stream from the original point
+ stream.Position = originalPosition;
+
+ // Return the string of the body
+ return responseText;
+ }
+ }
+}
diff --git a/src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddlewareOptions.cs b/src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddlewareOptions.cs
new file mode 100644
index 00000000..715000f2
--- /dev/null
+++ b/src/NLog.Web.AspNetCore/NLogRequestPostedBodyMiddlewareOptions.cs
@@ -0,0 +1,51 @@
+using System;
+using Microsoft.AspNetCore.Http;
+
+namespace NLog.Web
+{
+ ///
+ /// Contains the configuration for the NLogRequestPostedBodyMiddleware
+ ///
+ public class NLogRequestPostedBodyMiddlewareOptions
+ {
+ ///
+ /// The default configuration
+ ///
+ internal static readonly NLogRequestPostedBodyMiddlewareOptions Default = new NLogRequestPostedBodyMiddlewareOptions();
+
+ ///
+ /// The default constructor
+ ///
+ public NLogRequestPostedBodyMiddlewareOptions()
+ {
+ ShouldCapture = DefaultCapture;
+ }
+
+ ///
+ /// The maximum request size that will be captured
+ /// Defaults to 30KB. This checks against the ContentLength.
+ /// HttpRequest.EnableBuffer() writes the request to TEMP files on disk if the request ContentLength is > 30KB
+ /// but uses memory otherwise if <= 30KB, so we should protect against "very large"
+ /// request post body payloads.
+ ///
+ public int MaximumRequestSize { get; set; } = 30 * 1024;
+
+ ///
+ /// If this returns true, the post request body will be captured
+ /// Defaults to true if content length <= 30KB
+ /// This can be used to capture only certain content types,
+ /// only certain hosts, only below a certain request body size, and so forth
+ ///
+ ///
+ public Predicate ShouldCapture { get; set; }
+
+ ///
+ /// The default predicate for ShouldCapture
+ /// Returns true if content length <= 30KB
+ ///
+ private bool DefaultCapture(HttpContext context)
+ {
+ return context?.Request?.ContentLength != null && context?.Request?.ContentLength <= MaximumRequestSize;
+ }
+ }
+}
diff --git a/src/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRenderer.cs
new file mode 100644
index 00000000..c0d8f9f1
--- /dev/null
+++ b/src/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRenderer.cs
@@ -0,0 +1,65 @@
+using System.Text;
+using NLog.LayoutRenderers;
+#if ASP_NET_CORE
+using Microsoft.AspNetCore.Http;
+#else
+using System.Web;
+#endif
+namespace NLog.Web.LayoutRenderers
+{
+ ///
+ /// ASP.NET posted body, e.g. FORM or Ajax POST
+ ///
+ /// Example usage of ${aspnet-request-posted-body}:
+ ///
+ ///
+ /// ${aspnet-request-posted-body} - Produces - {username:xyz,password:xyz}
+ ///
+ ///
+ [LayoutRenderer("aspnet-request-posted-body")]
+ public class AspNetRequestPostedBodyLayoutRenderer : AspNetLayoutRendererBase
+ {
+
+ ///
+ /// The object for the key in HttpContext.Items for the POST request body
+ ///
+ internal static readonly object NLogPostedRequestBodyKey = new object();
+
+ /// Renders the ASP.NET posted body
+ /// The to append the rendered data to.
+ /// Logging event.
+ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
+ {
+ var httpContext = HttpContextAccessor.HttpContext;
+ if (httpContext == null)
+ {
+ return;
+ }
+
+ var items = httpContext.Items;
+ if (items == null)
+ {
+ return;
+ }
+
+ if (httpContext.Items.Count == 0)
+ {
+ return;
+ }
+
+#if !ASP_NET_CORE
+ if (!items.Contains(NLogPostedRequestBodyKey))
+ {
+ return;
+ }
+#else
+ if (!items.ContainsKey(NLogPostedRequestBodyKey))
+ {
+ return;
+ }
+#endif
+
+ builder.Append(items[NLogPostedRequestBodyKey] as string);
+ }
+ }
+}
diff --git a/tests/NLog.Web.AspNetCore.Tests/NLogRequestPostedBodyMiddlewareOptionsTests.cs b/tests/NLog.Web.AspNetCore.Tests/NLogRequestPostedBodyMiddlewareOptionsTests.cs
new file mode 100644
index 00000000..ef6743ba
--- /dev/null
+++ b/tests/NLog.Web.AspNetCore.Tests/NLogRequestPostedBodyMiddlewareOptionsTests.cs
@@ -0,0 +1,76 @@
+using System;
+using Microsoft.AspNetCore.Http;
+using NSubstitute;
+using Xunit;
+
+namespace NLog.Web.Tests
+{
+ public class NLogRequestPostedBodyMiddlewareOptionsTests
+ {
+ [Fact]
+ public void SetMaximumRequestSizeTest()
+ {
+ var config = new NLogRequestPostedBodyMiddlewareOptions();
+ var size = new Random().Next();
+ config.MaximumRequestSize = size;
+
+ Assert.Equal(size, config.MaximumRequestSize);
+ }
+
+ [Fact]
+ public void GetDefault()
+ {
+ var config = NLogRequestPostedBodyMiddlewareOptions.Default;
+
+ Assert.NotNull(config);
+ }
+
+ [Fact]
+ public void DefaultCaptureTrue()
+ {
+ var config = NLogRequestPostedBodyMiddlewareOptions.Default;
+
+ HttpContext httpContext = Substitute.For();
+
+ HttpRequest request = Substitute.For();
+
+ request.ContentLength.Returns(NLogRequestPostedBodyMiddlewareOptions.Default.MaximumRequestSize - 1);
+
+ httpContext.Request.Returns(request);
+
+ Assert.True(config.ShouldCapture(httpContext));
+ }
+
+ [Fact]
+ public void DefaultCaptureFalseNullContentLength()
+ {
+ var config = NLogRequestPostedBodyMiddlewareOptions.Default;
+
+ HttpContext httpContext = Substitute.For();
+
+ HttpRequest request = Substitute.For();
+
+ request.ContentLength.Returns((long?)null);
+
+ httpContext.Request.Returns(request);
+
+ Assert.False(config.ShouldCapture(httpContext));
+ }
+
+ [Fact]
+ public void DefaultCaptureExcessiveContentLength()
+ {
+ var config = NLogRequestPostedBodyMiddlewareOptions.Default;
+
+ HttpContext httpContext = Substitute.For();
+
+ HttpRequest request = Substitute.For();
+
+ request.ContentLength.Returns(NLogRequestPostedBodyMiddlewareOptions.Default.MaximumRequestSize + 1);
+
+ httpContext.Request.Returns(request);
+
+ Assert.False(config.ShouldCapture(httpContext));
+ }
+ }
+}
diff --git a/tests/NLog.Web.AspNetCore.Tests/NLogRequestPostedBodyMiddlewareTests.cs b/tests/NLog.Web.AspNetCore.Tests/NLogRequestPostedBodyMiddlewareTests.cs
new file mode 100644
index 00000000..aeeaa76c
--- /dev/null
+++ b/tests/NLog.Web.AspNetCore.Tests/NLogRequestPostedBodyMiddlewareTests.cs
@@ -0,0 +1,195 @@
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using NLog.Web.LayoutRenderers;
+using NSubstitute;
+using NSubstitute.ReturnsExtensions;
+using Xunit;
+
+namespace NLog.Web.Tests
+{
+ public class NLogRequestPostedBodyMiddlewareTests
+ {
+ ///
+ /// This acts as a parameter for the RequestDelegate parameter for the middleware InvokeAsync method
+ ///
+ ///
+ ///
+ private static Task Next(HttpContext context)
+ {
+ return Task.CompletedTask;
+ }
+
+ [Fact]
+ public void SuccessTest()
+ {
+ // Arrange
+ DefaultHttpContext defaultContext = new DefaultHttpContext();
+ defaultContext.Request.Body = new MemoryStream();
+ byte[] bodyBytes = Encoding.UTF8.GetBytes("This is a test request body");
+ defaultContext.Request.Body.Write(bodyBytes,0,bodyBytes.Length);
+ defaultContext.Request.ContentLength = bodyBytes.Length;
+
+ // Act
+
+ long streamBeforePosition = defaultContext.Request.Body.Position;
+
+ var middlewareInstance = new NLogRequestPostedBodyMiddleware(Next, NLogRequestPostedBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ long streamAfterPosition = defaultContext.Request.Body.Position;
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Single(defaultContext.Items);
+ Assert.NotNull(defaultContext.Items[AspNetRequestPostedBodyLayoutRenderer.NLogPostedRequestBodyKey]);
+ Assert.True(defaultContext.Items[AspNetRequestPostedBodyLayoutRenderer.NLogPostedRequestBodyKey] is string);
+ Assert.Equal("This is a test request body", defaultContext.Items[AspNetRequestPostedBodyLayoutRenderer.NLogPostedRequestBodyKey] as string);
+ Assert.Equal(streamBeforePosition, streamAfterPosition);
+ }
+
+ [Fact]
+ public void EmptyBodyTest()
+ {
+ // Arrange
+ DefaultHttpContext defaultContext = new DefaultHttpContext();
+ defaultContext.Request.Body = new MemoryStream();
+ defaultContext.Request.ContentLength = 0;
+
+ // Act
+ var middlewareInstance = new NLogRequestPostedBodyMiddleware(Next, NLogRequestPostedBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Empty(defaultContext.Items);
+ Assert.Null(defaultContext.Items[AspNetRequestPostedBodyLayoutRenderer.NLogPostedRequestBodyKey]);
+ }
+
+ [Fact]
+ public void NullContextTest()
+ {
+ // Arrange
+ HttpContext defaultContext = null;
+
+ // Act
+ var middlewareInstance = new NLogRequestPostedBodyMiddleware(Next, NLogRequestPostedBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ // Assert that we got to this point without NullReferenceException
+ Assert.True(true);
+ }
+
+ [Fact]
+ public void NullRequestTest()
+ {
+ // Arrange
+ HttpContext defaultContext = Substitute.For();
+ defaultContext.Request.ReturnsNull();
+
+ // Act
+ var middlewareInstance = new NLogRequestPostedBodyMiddleware(Next, NLogRequestPostedBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Empty(defaultContext.Items);
+ }
+
+ [Fact]
+ public void NullBodyTest()
+ {
+ // Arrange
+ DefaultHttpContext defaultContext = new DefaultHttpContext();
+ defaultContext.Request.Body = null;
+
+ // Act
+ var middlewareInstance = new NLogRequestPostedBodyMiddleware(Next,NLogRequestPostedBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Empty(defaultContext.Items);
+ }
+
+ [Fact]
+ public void ContentLengthTooLargeTest()
+ {
+ // Arrange
+ DefaultHttpContext defaultContext = new DefaultHttpContext();
+ defaultContext.Request.Body = new MemoryStream();
+ defaultContext.Request.Body.Write(new byte[30 * 1024 + 1],0, 30 * 1024 + 1);
+ defaultContext.Request.ContentLength = 30 * 1024 + 1;
+
+ // Act
+ var middlewareInstance = new NLogRequestPostedBodyMiddleware(Next,NLogRequestPostedBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Empty(defaultContext.Items);
+ }
+
+ [Fact]
+ public void MissingContentLengthTest()
+ {
+ // Arrange
+ DefaultHttpContext defaultContext = new DefaultHttpContext();
+ defaultContext.Request.Body = new MemoryStream();
+ defaultContext.Request.Body.Write(new byte[128],0,128);
+ defaultContext.Request.ContentLength = null;
+
+ // Act
+ var middlewareInstance = new NLogRequestPostedBodyMiddleware(Next,NLogRequestPostedBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Empty(defaultContext.Items);
+ }
+
+ [Fact]
+ public void CannotReadLengthTest()
+ {
+ // Arrange
+ DefaultHttpContext defaultContext = new DefaultHttpContext();
+
+ defaultContext.Request.Body = Substitute.For();
+
+ defaultContext.Request.Body.CanRead.Returns(false);
+ defaultContext.Request.Body.CanSeek.Returns(true);
+
+ // Act
+ var middlewareInstance =
+ new NLogRequestPostedBodyMiddleware(Next,NLogRequestPostedBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Empty(defaultContext.Items);
+ }
+
+ [Fact]
+ public void CannotSeekLengthTest()
+ {
+ // Arrange
+ DefaultHttpContext defaultContext = new DefaultHttpContext();
+
+ defaultContext.Request.Body = Substitute.For();
+
+ defaultContext.Request.Body.CanRead.Returns(true);
+ defaultContext.Request.Body.CanSeek.Returns(false);
+
+ // Act
+ var middlewareInstance =
+ new NLogRequestPostedBodyMiddleware(Next,NLogRequestPostedBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Empty(defaultContext.Items);
+ }
+ }
+}
diff --git a/tests/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRendererTests.cs b/tests/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRendererTests.cs
new file mode 100644
index 00000000..9b774c63
--- /dev/null
+++ b/tests/Shared/LayoutRenderers/AspNetRequestPostedBodyLayoutRendererTests.cs
@@ -0,0 +1,106 @@
+using System.Collections.Generic;
+#if ASP_NET_CORE
+using Microsoft.AspNetCore.Http;
+#else
+using System.Web;
+#endif
+using NLog.Web.LayoutRenderers;
+using NSubstitute;
+using NSubstitute.ReturnsExtensions;
+using Xunit;
+
+namespace NLog.Web.Tests.LayoutRenderers
+{
+ public class AspNetRequestPostedBodyLayoutRendererTests : LayoutRenderersTestBase
+ {
+ [Fact]
+ public void RequestPostedBodyPresentRenderNonEmptyString()
+ {
+ // Arrange
+ var (renderer, httpContext) = CreateWithHttpContext();
+ string expected = "This is a test of the request posted body layout renderer.";
+ var items = new Dictionary