diff --git a/src/NLog.Web.AspNetCore/NLog.Web.AspNetCore.csproj b/src/NLog.Web.AspNetCore/NLog.Web.AspNetCore.csproj
index 44160cbd..673de83f 100644
--- a/src/NLog.Web.AspNetCore/NLog.Web.AspNetCore.csproj
+++ b/src/NLog.Web.AspNetCore/NLog.Web.AspNetCore.csproj
@@ -75,6 +75,8 @@ NLog 5 release post: https://nlog-project.org/2021/08/25/nlog-5-0-preview1-ready
$(DefineConstants);ASP_NET_CORE;ASP_NET_CORE3
+
+
@@ -100,7 +102,7 @@ NLog 5 release post: https://nlog-project.org/2021/08/25/nlog-5-0-preview1-ready
-
+
true
\
diff --git a/src/NLog.Web.AspNetCore/NLogResponseBodyMiddleware.cs b/src/NLog.Web.AspNetCore/NLogResponseBodyMiddleware.cs
new file mode 100644
index 00000000..c8797e52
--- /dev/null
+++ b/src/NLog.Web.AspNetCore/NLogResponseBodyMiddleware.cs
@@ -0,0 +1,169 @@
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.IO;
+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
+ ///
+ /// Response body
+ ///
+ /// The following are saved in the HttpContext.Items collection
+ ///
+ /// __nlog-aspnet-response-body
+ ///
+ /// Usage: app.UseMiddleware<NLogResponseBodyMiddleware>(); where app is an IApplicationBuilder
+ /// Register the NLogBodyMiddlewareOptions in the IoC so that the config gets passed to the constructor
+ /// Please use with caution, this will temporarily use 2x the memory for the response, so this may be
+ /// suitable only for responses < 64 KB
+ ///
+ public class NLogResponseBodyMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ private readonly NLogResponseBodyMiddlewareOptions _options;
+
+ // Using this instead of new MemoryStream() is important to the performance.
+ // According to the articles, this should be used as a static and not as an instance.
+ // This will manage a pool of MemoryStream instead of creating a new MemoryStream every response.
+ private static readonly RecyclableMemoryStreamManager Manager = new RecyclableMemoryStreamManager();
+
+ ///
+ /// Constructor that takes a configuration
+ ///
+ ///
+ ///
+ public NLogResponseBodyMiddleware(RequestDelegate next, NLogResponseBodyMiddlewareOptions options)
+ {
+ _next = next;
+ _options = options;
+ }
+
+ ///
+ /// This allows interception of the HTTP pipeline for logging purposes
+ ///
+ ///
+ ///
+ public async Task Invoke(HttpContext context)
+ {
+ if (ShouldCaptureResponseBody(context))
+ {
+ using (var memoryStream = Manager.GetStream())
+ {
+ // Save away the true response stream
+ var originalStream = context.Response.Body;
+
+ // Make the Http Context Response Body refer to the Memory Stream
+ context.Response.Body = memoryStream;
+
+ // The Http Context Response then writes to the Memory Stream
+ await _next(context).ConfigureAwait(false);
+
+ var responseBody = await GetString(memoryStream).ConfigureAwait(false);
+
+ // Copy the contents of the memory stream back to the true response stream
+ await memoryStream.CopyToAsync(originalStream).ConfigureAwait(false);
+
+ // This next line enables NLog to log the response
+ if (!string.IsNullOrEmpty(responseBody) && _options.ShouldRetainCapture(context))
+ {
+ context.Items[AspNetResponseBodyLayoutRenderer.NLogResponseBodyKey] = responseBody;
+ }
+ }
+ }
+ else
+ {
+ if (context != null)
+ {
+ await _next(context).ConfigureAwait(false);
+ }
+ }
+ }
+
+ private bool ShouldCaptureResponseBody(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;
+ }
+
+ // Perform null checking
+ if (context.Response == null)
+ {
+ InternalLogger.Debug("NLogResponseBodyMiddleware: HttpContext.Response is null");
+ return false;
+ }
+
+ // Perform null checking
+ if (context.Response.Body == null)
+ {
+ InternalLogger.Debug("NLogResponseBodyMiddleware: HttpContext.Response.Body stream is null");
+ return false;
+ }
+
+ // If we cannot write the response stream we cannot capture the body
+ if (!context.Response.Body.CanWrite)
+ {
+ InternalLogger.Debug("NLogResponseBodyMiddleware: HttpContext.Response.Body stream is non-writeable");
+ return false;
+ }
+
+ // Use the predicate in the configuration instance that takes the HttpContext as an argument
+ if (!_options.ShouldCapture(context))
+ {
+ InternalLogger.Debug("NLogResponseBodyMiddleware: _configuration.ShouldCapture(HttpContext) predicate returned false");
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ /// 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/NLogResponseBodyMiddlewareOptions.cs b/src/NLog.Web.AspNetCore/NLogResponseBodyMiddlewareOptions.cs
new file mode 100644
index 00000000..874826fb
--- /dev/null
+++ b/src/NLog.Web.AspNetCore/NLogResponseBodyMiddlewareOptions.cs
@@ -0,0 +1,63 @@
+using System;
+using Microsoft.AspNetCore.Http;
+
+namespace NLog.Web
+{
+ ///
+ /// Contains the configuration for the NLogRequestPostedBodyMiddleware
+ ///
+ public class NLogResponseBodyMiddlewareOptions
+ {
+ ///
+ /// The default configuration
+ ///
+ internal static readonly NLogResponseBodyMiddlewareOptions Default = new NLogResponseBodyMiddlewareOptions();
+
+ ///
+ /// Default Constructor
+ ///
+ public NLogResponseBodyMiddlewareOptions()
+ {
+ ShouldRetainCapture = DefaultRetainCapture;
+ ShouldCapture = DefaultShouldCapture;
+ }
+
+ ///
+ /// The maximum response size that will be captured
+ /// Defaults to 30KB
+ ///
+ public int MaximumResponseSize { get; set; } = 30 * 1024;
+
+ ///
+ /// If this returns true, the response body will be captured
+ /// Defaults to true
+ /// 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
+ ///
+ private bool DefaultShouldCapture(HttpContext context)
+ {
+ return true;
+ }
+
+ ///
+ /// Defaults to true if content length <= 30KB
+ ///
+ public Predicate ShouldRetainCapture { get; set; }
+
+ ///
+ /// The default predicate for ShouldCapture
+ /// Returns true if content length <= 30KB
+ ///
+ private bool DefaultRetainCapture(HttpContext context)
+ {
+ return context?.Response?.ContentLength != null && context?.Response?.ContentLength <= MaximumResponseSize;
+ }
+ }
+}
diff --git a/src/Shared/Internal/HttpContextExtensions.cs b/src/Shared/Internal/HttpContextExtensions.cs
index c90c8917..9abedc0a 100644
--- a/src/Shared/Internal/HttpContextExtensions.cs
+++ b/src/Shared/Internal/HttpContextExtensions.cs
@@ -62,42 +62,6 @@ internal static HttpResponse TryGetResponse(this HttpContext context)
}
#endif
-#if ASP_NET_CORE2
- internal static string GetString(this ISession session, string key)
- {
- if (!session.TryGetValue(key, out var data))
- {
- return null;
- }
-
- if (data == null)
- {
- return null;
- }
-
- if (data.Length == 0)
- {
- return string.Empty;
- }
-
- return Encoding.UTF8.GetString(data);
- }
-
- public static int? GetInt32(this ISession session, string key)
- {
- if (!session.TryGetValue(key, out var data))
- {
- return null;
- }
-
- if (data == null || data.Length < 4)
- {
- return null;
- }
- return data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3];
- }
-#endif
-
#if !ASP_NET_CORE
internal static HttpSessionStateBase TryGetSession(this HttpContextBase context)
{
diff --git a/src/Shared/LayoutRenderers/AspNetLayoutMultiValueRendererBase.cs b/src/Shared/LayoutRenderers/AspNetLayoutMultiValueRendererBase.cs
index 160ceefa..19a3ee04 100644
--- a/src/Shared/LayoutRenderers/AspNetLayoutMultiValueRendererBase.cs
+++ b/src/Shared/LayoutRenderers/AspNetLayoutMultiValueRendererBase.cs
@@ -170,7 +170,7 @@ private void SerializePairsFlat(IEnumerable> pairs,
#endregion
-
+
#region Singles
///
@@ -193,7 +193,7 @@ protected void SerializeSingles(IEnumerable values, StringBuilder builde
}
}
- private void SerializeSinglesJson(IEnumerable values, StringBuilder builder)
+ private static void SerializeSinglesJson(IEnumerable values, StringBuilder builder)
{
var firstItem = true;
foreach (var item in values)
diff --git a/src/Shared/LayoutRenderers/AspNetResponseBodyLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetResponseBodyLayoutRenderer.cs
new file mode 100644
index 00000000..c28c25b6
--- /dev/null
+++ b/src/Shared/LayoutRenderers/AspNetResponseBodyLayoutRenderer.cs
@@ -0,0 +1,58 @@
+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 response body
+ ///
+ [LayoutRenderer("aspnet-response-body")]
+ public class AspNetResponseBodyLayoutRenderer : AspNetLayoutRendererBase
+ {
+
+ ///
+ /// The object for the key in HttpContext.Items for the response body
+ ///
+ internal static readonly object NLogResponseBodyKey = new object();
+
+ /// Renders the ASP.NET response 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(NLogResponseBodyKey))
+ {
+ return;
+ }
+#else
+ if (!items.ContainsKey(NLogResponseBodyKey))
+ {
+ return;
+ }
+#endif
+ builder.Append(items[NLogResponseBodyKey] as string);
+ }
+ }
+}
diff --git a/src/Shared/LayoutRenderers/AspNetResponseCookieLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetResponseCookieLayoutRenderer.cs
new file mode 100644
index 00000000..66b3b628
--- /dev/null
+++ b/src/Shared/LayoutRenderers/AspNetResponseCookieLayoutRenderer.cs
@@ -0,0 +1,185 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using NLog.Config;
+using NLog.LayoutRenderers;
+using NLog.Web.Enums;
+using NLog.Web.Internal;
+#if !ASP_NET_CORE
+using System.Collections.Specialized;
+using System.Web;
+using Cookies = System.Web.HttpCookieCollection;
+#else
+using Microsoft.Net.Http.Headers;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
+#endif
+
+namespace NLog.Web.LayoutRenderers
+{
+ ///
+ /// ASP.NET Response Cookie
+ ///
+ ///
+ /// Example usage of ${aspnet-response-cookie}
+ ///
+ /// ${aspnet-response-cookie:OutputFormat=Flat}
+ /// ${aspnet-response-cookie:OutputFormat=JsonArray}
+ /// ${aspnet-response-cookie:OutputFormat=JsonDictionary}
+ /// ${aspnet-response-cookie:OutputFormat=JsonDictionary:CookieNames=username}
+ /// ${aspnet-response-cookie:OutputFormat=JsonDictionary:Exclude=access_token}
+ ///
+ ///
+ [LayoutRenderer("aspnet-response-cookie")]
+ public class AspNetResponseCookieLayoutRenderer : AspNetLayoutMultiValueRendererBase
+ {
+ ///
+ /// Cookie names to be rendered.
+ /// If null or empty array, all cookies will be rendered.
+ ///
+ public List CookieNames { get; set; }
+
+ ///
+ /// Gets or sets the keys to exclude from the output. If omitted, none are excluded.
+ ///
+ ///
+#if ASP_NET_CORE
+ public ISet Exclude { get; set; }
+#else
+ public HashSet Exclude { get; set; }
+#endif
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AspNetResponseCookieLayoutRenderer()
+ {
+ Exclude = new HashSet(new[] { "AUTH", "SESS_ID" }, StringComparer.OrdinalIgnoreCase);
+ }
+
+#if !ASP_NET_CORE
+ ///
+ /// Renders the ASP.NET Cookie appends it to the specified .
+ ///
+ /// The to append the rendered data to.
+ /// Logging event.
+ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
+ {
+ var httpResponse = HttpContextAccessor.HttpContext.TryGetResponse();
+ if (httpResponse == null)
+ {
+ return;
+ }
+
+ var cookies = httpResponse.Cookies;
+
+ if (cookies?.Count > 0)
+ {
+ bool checkForExclude = (CookieNames == null || CookieNames.Count == 0) && Exclude?.Count > 0;
+ var cookieValues = GetCookieValues(cookies, checkForExclude);
+ SerializePairs(cookieValues, builder, logEvent);
+ }
+ }
+
+ private IEnumerable> GetCookieValues(HttpCookieCollection cookies, bool checkForExclude)
+ {
+ var cookieNames = GetCookieNames(cookies);
+ foreach (var cookieName in cookieNames)
+ {
+ if (checkForExclude && Exclude.Contains(cookieName))
+ continue;
+
+ var httpCookie = cookies[cookieName];
+ if (httpCookie == null)
+ {
+ continue;
+ }
+
+ if (OutputFormat != AspNetRequestLayoutOutputFormat.Flat)
+ {
+ // Split multi-valued cookie, as allowed for in the HttpCookie API for backwards compatibility with classic ASP
+ var isFirst = true;
+ foreach (var multiValueKey in httpCookie.Values.AllKeys)
+ {
+ var cookieKey = multiValueKey;
+ if (isFirst)
+ {
+ cookieKey = cookieName;
+ isFirst = false;
+ }
+ yield return new KeyValuePair(cookieKey, httpCookie.Values[multiValueKey]);
+ }
+ }
+ else
+ {
+ yield return new KeyValuePair(cookieName, httpCookie.Value);
+ }
+ }
+ }
+
+ private List GetCookieNames(HttpCookieCollection cookies)
+ {
+ return CookieNames?.Count > 0 ? CookieNames : cookies.Keys.Cast().ToList();
+ }
+#else
+ ///
+ /// Renders the ASP.NET Cookie appends it to the specified .
+ ///
+ /// The to append the rendered data to.
+ /// Logging event.
+ protected override void DoAppend(StringBuilder builder, LogEventInfo logEvent)
+ {
+ var httpResponse = HttpContextAccessor.HttpContext.TryGetResponse();
+ if (httpResponse == null)
+ {
+ return;
+ }
+
+ IList cookies = httpResponse.GetTypedHeaders().SetCookie;
+
+ if (cookies?.Count > 0)
+ {
+ bool checkForExclude = (CookieNames == null || CookieNames.Count == 0) && Exclude?.Count > 0;
+ var cookieValues = GetCookieValues(cookies, checkForExclude);
+ SerializePairs(cookieValues, builder, logEvent);
+ }
+ }
+
+ private List GetCookieNames(IList cookies)
+ {
+ if (CookieNames?.Count > 0)
+ {
+ return CookieNames;
+ }
+
+ var response = new List();
+
+ foreach (var cookie in cookies)
+ {
+ response.Add(cookie.Name.ToString());
+ }
+
+ return response;
+ }
+
+ private IEnumerable> GetCookieValues(IList cookies, bool checkForExclude)
+ {
+ var cookieNames = GetCookieNames(cookies);
+ foreach (var cookieName in cookieNames)
+ {
+ if (checkForExclude && Exclude.Contains(cookieName))
+ continue;
+
+ var httpCookie = cookies.SingleOrDefault(cookie => cookie.Name.ToString() == cookieName);
+ if (httpCookie == null)
+ {
+ continue;
+ }
+
+ yield return new KeyValuePair(cookieName, httpCookie.Value.ToString());
+ }
+ }
+#endif
+ }
+}
\ No newline at end of file
diff --git a/tests/NLog.Web.AspNetCore.Tests/NLogResponseBodyMiddlewareOptionsTests.cs b/tests/NLog.Web.AspNetCore.Tests/NLogResponseBodyMiddlewareOptionsTests.cs
new file mode 100644
index 00000000..d00661ff
--- /dev/null
+++ b/tests/NLog.Web.AspNetCore.Tests/NLogResponseBodyMiddlewareOptionsTests.cs
@@ -0,0 +1,76 @@
+using System;
+using Microsoft.AspNetCore.Http;
+using NSubstitute;
+using Xunit;
+
+namespace NLog.Web.Tests
+{
+ public class NLogResponseBodyMiddlewareOptionsTests
+ {
+ [Fact]
+ public void SetMaximumRequestSizeTest()
+ {
+ var config = new NLogResponseBodyMiddlewareOptions();
+ var size = new Random().Next();
+ config.MaximumResponseSize = size;
+
+ Assert.Equal(size, config.MaximumResponseSize);
+ }
+
+ [Fact]
+ public void GetDefault()
+ {
+ var config = NLogResponseBodyMiddlewareOptions.Default;
+
+ Assert.NotNull(config);
+ }
+
+ [Fact]
+ public void DefaultCaptureTrue()
+ {
+ var config = NLogResponseBodyMiddlewareOptions.Default;
+
+ HttpContext httpContext = Substitute.For();
+
+ HttpResponse response = Substitute.For();
+
+ response.ContentLength.Returns(NLogResponseBodyMiddlewareOptions.Default.MaximumResponseSize - 1);
+
+ httpContext.Response.Returns(response);
+
+ Assert.True(config.ShouldRetainCapture(httpContext));
+ }
+
+ [Fact]
+ public void DefaultCaptureFalseNullContentLength()
+ {
+ var config = NLogResponseBodyMiddlewareOptions.Default;
+
+ HttpContext httpContext = Substitute.For();
+
+ HttpResponse response = Substitute.For();
+
+ response.ContentLength.Returns((long?)null);
+
+ httpContext.Response.Returns(response);
+
+ Assert.False(config.ShouldRetainCapture(httpContext));
+ }
+
+ [Fact]
+ public void DefaultCaptureExcessiveContentLength()
+ {
+ var config = NLogResponseBodyMiddlewareOptions.Default;
+
+ HttpContext httpContext = Substitute.For();
+
+ HttpResponse response = Substitute.For();
+
+ response.ContentLength.Returns(NLogResponseBodyMiddlewareOptions.Default.MaximumResponseSize + 1);
+
+ httpContext.Response.Returns(response);
+
+ Assert.False(config.ShouldRetainCapture(httpContext));
+ }
+ }
+}
diff --git a/tests/NLog.Web.AspNetCore.Tests/NLogResponseBodyMiddlewareTests.cs b/tests/NLog.Web.AspNetCore.Tests/NLogResponseBodyMiddlewareTests.cs
new file mode 100644
index 00000000..2d03f965
--- /dev/null
+++ b/tests/NLog.Web.AspNetCore.Tests/NLogResponseBodyMiddlewareTests.cs
@@ -0,0 +1,101 @@
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using NLog.Web.LayoutRenderers;
+using NSubstitute;
+using Xunit;
+
+namespace NLog.Web.Tests
+{
+ public class NLogResponseBodyMiddlewareTests
+ {
+ ///
+ /// This acts as a parameter for the RequestDelegate parameter for the middleware InvokeAsync method
+ ///
+ ///
+ ///
+ private Task Next(HttpContext context)
+ {
+ byte[] bodyBytes = Encoding.UTF8.GetBytes("This is a test response body");
+ context.Response.Body.Write(bodyBytes, 0, bodyBytes.Length);
+ return Task.CompletedTask;
+ }
+
+ ///
+ /// This acts as a parameter for the RequestDelegate parameter for the middleware InvokeAsync method
+ ///
+ ///
+ ///
+ private Task NextNone(HttpContext context)
+ {
+ return Task.CompletedTask;
+ }
+
+ [Fact]
+ public void SuccessTest()
+ {
+ // Arrange
+ DefaultHttpContext defaultContext = new DefaultHttpContext();
+ defaultContext.Response.Body = new MemoryStream();
+ defaultContext.Response.ContentLength = "This is a test response body".Length;
+
+ // Act
+ var middlewareInstance = new NLogResponseBodyMiddleware(Next,NLogResponseBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Single(defaultContext.Items);
+ Assert.NotNull(defaultContext.Items[AspNetResponseBodyLayoutRenderer.NLogResponseBodyKey]);
+ Assert.True(defaultContext.Items[AspNetResponseBodyLayoutRenderer.NLogResponseBodyKey] is string);
+ Assert.Equal("This is a test response body", defaultContext.Items[AspNetResponseBodyLayoutRenderer.NLogResponseBodyKey] as string);
+ }
+
+ [Fact]
+ public void NullContextTest()
+ {
+ // Arrange
+ HttpContext defaultContext = null;
+
+ // Act
+ var middlewareInstance = new NLogResponseBodyMiddleware(Next, NLogResponseBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ // Assert that we got to this point without NullReferenceException
+ Assert.True(true);
+ }
+
+ [Fact]
+ public void EmptyBodyTest()
+ {
+ // Arrange
+ DefaultHttpContext defaultContext = new DefaultHttpContext();
+ defaultContext.Response.Body = new MemoryStream();
+
+ // Act
+ var middlewareInstance = new NLogResponseBodyMiddleware(NextNone,NLogResponseBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Empty(defaultContext.Items);
+ }
+
+ [Fact]
+ public void NullResponseTest()
+ {
+ // Arrange
+ HttpContext defaultContext = Substitute.For();
+ defaultContext.Response.Body.Returns((Stream) null);
+
+ var middlewareInstance = new NLogResponseBodyMiddleware(NextNone,NLogResponseBodyMiddlewareOptions.Default);
+ middlewareInstance.Invoke(defaultContext).ConfigureAwait(false).GetAwaiter().GetResult();
+
+ // Assert
+ Assert.NotNull(defaultContext.Items);
+ Assert.Empty(defaultContext.Items);
+ }
+ }
+}
diff --git a/tests/Shared/LayoutRenderers/AspNetResponseBodyLayoutRendererTests.cs b/tests/Shared/LayoutRenderers/AspNetResponseBodyLayoutRendererTests.cs
new file mode 100644
index 00000000..2deb1db4
--- /dev/null
+++ b/tests/Shared/LayoutRenderers/AspNetResponseBodyLayoutRendererTests.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 AspNetResponseBodyLayoutRendererTests : LayoutRenderersTestBase
+ {
+ [Fact]
+ public void RequestPostedBodyPresentRenderNonEmptyString()
+ {
+ // Arrange
+ var (renderer, httpContext) = CreateWithHttpContext();
+ string expected = "This is a test of the response body layout renderer.";
+ var items = new Dictionary