diff --git a/src/NLog.Web.AspNetCore/NLog.Web.AspNetCore.csproj b/src/NLog.Web.AspNetCore/NLog.Web.AspNetCore.csproj
index 44160cbd..6c15ab03 100644
--- a/src/NLog.Web.AspNetCore/NLog.Web.AspNetCore.csproj
+++ b/src/NLog.Web.AspNetCore/NLog.Web.AspNetCore.csproj
@@ -75,7 +75,7 @@ NLog 5 release post: https://nlog-project.org/2021/08/25/nlog-5-0-preview1-ready
$(DefineConstants);ASP_NET_CORE;ASP_NET_CORE3
-
+
diff --git a/src/Shared/LayoutRenderers/AspNetResponseCookieLayoutRenderer.cs b/src/Shared/LayoutRenderers/AspNetResponseCookieLayoutRenderer.cs
new file mode 100644
index 00000000..b63f5cfc
--- /dev/null
+++ b/src/Shared/LayoutRenderers/AspNetResponseCookieLayoutRenderer.cs
@@ -0,0 +1,180 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+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.Extensions.Primitives;
+using Microsoft.Net.Http.Headers;
+using Microsoft.AspNetCore.Http;
+#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);
+ }
+
+ ///
+ /// 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 = GetCookies(httpResponse);
+ if (cookies.Count > 0)
+ {
+ bool checkForExclude = (CookieNames == null || CookieNames.Count == 0) && Exclude?.Count > 0;
+ var cookieValues = GetCookieValues(cookies, checkForExclude);
+ SerializePairs(cookieValues, builder, logEvent);
+ }
+ }
+
+#if !ASP_NET_CORE
+
+ ///
+ /// Method to get cookies for .NET Framework
+ ///
+ ///
+ ///
+ private Cookies GetCookies(HttpResponseBase response)
+ {
+ return response.Cookies;
+ }
+
+ private List GetCookieNames(HttpCookieCollection cookies)
+ {
+ return CookieNames?.Count > 0 ? CookieNames : cookies.Keys.Cast().ToList();
+ }
+
+ private IEnumerable> GetCookieValues(HttpCookieCollection cookies, bool checkForExclude)
+ {
+ var response = new List>();
+ var cookieNames = GetCookieNames(cookies);
+ foreach (var cookieName in cookieNames)
+ {
+ if (checkForExclude && Exclude.Contains(cookieName))
+ continue;
+
+ var httpCookie = cookies[cookieName];
+ if (httpCookie == null)
+ {
+ continue;
+ }
+ response.AddRange(GetCookieValue(httpCookie,cookieName));
+ }
+ return response;
+ }
+
+ private IEnumerable> GetCookieValue(HttpCookie httpCookie, string cookieName)
+ {
+ 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);
+ }
+ }
+
+#else
+ ///
+ /// Method to get cookies for all ASP.NET Core versions
+ ///
+ ///
+ ///
+ private static IList GetCookies(HttpResponse response)
+ {
+ var queryResults = response.Headers[HeaderNames.SetCookie];
+ if (queryResults.Count > 0 && SetCookieHeaderValue.TryParseList(queryResults, out var result))
+ return result;
+ else
+ return Array.Empty();
+ }
+
+ private List GetCookieNames(IEnumerable cookies)
+ {
+ return CookieNames?.Count > 0 ? CookieNames : cookies.Select(row => row.Name.ToString()).ToList();
+ }
+
+ private IEnumerable> GetCookieValues(IEnumerable 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/Shared/LayoutRenderers/AspNetResponseCookieLayoutRendererTests.cs b/tests/Shared/LayoutRenderers/AspNetResponseCookieLayoutRendererTests.cs
new file mode 100644
index 00000000..49a9b938
--- /dev/null
+++ b/tests/Shared/LayoutRenderers/AspNetResponseCookieLayoutRendererTests.cs
@@ -0,0 +1,434 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NLog.Web.LayoutRenderers;
+using NLog.Web.Enums;
+using Xunit;
+#if !ASP_NET_CORE
+using NSubstitute;
+using System.Web;
+#else
+using Microsoft.Extensions.Primitives;
+#endif
+
+namespace NLog.Web.Tests.LayoutRenderers
+{
+ public class AspNetResponseCookieLayoutRendererTests : TestInvolvingAspNetHttpContext
+ {
+ [Fact]
+ public void NullKeyRendersAllCookies()
+ {
+ var expectedResult = "key=TEST,Key1=TEST1";
+ var renderer = CreateRenderer();
+ renderer.CookieNames = null;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void NullKeyRendersAllCookiesExceptExcluded()
+ {
+ var expectedResult = "Key1=TEST1";
+ var renderer = CreateRenderer();
+ renderer.CookieNames = null;
+ renderer.Exclude.Add("key");
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyNotFoundRendersEmptyString_Flat_Formatting()
+ {
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Flat;
+ renderer.CookieNames = new List { "notfound" };
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void KeyNotFoundRendersEmptyString_Json_Formatting()
+ {
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.JsonArray;
+ renderer.CookieNames = new List { "notfound" };
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Multiple_Cookies_Flat_Formatting()
+ {
+ var expectedResult = "key=TEST,Key1=TEST1";
+
+ var renderer = CreateRenderer();
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Multiple_Cookies_Flat_Formatting_separators()
+ {
+ var expectedResult = "key:TEST|Key1:TEST1";
+
+ var renderer = CreateRenderer();
+ renderer.ValueSeparator = ":";
+ renderer.ItemSeparator = "|";
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Multiple_Cookies_Flat_Formatting_separators_layouts()
+ {
+ var expectedResult = "key>TEST" + Environment.NewLine + "Key1>TEST1";
+
+ var renderer = CreateRenderer();
+ renderer.ValueSeparator = "${event-properties:valueSeparator1}";
+ renderer.ItemSeparator = "${newline}";
+
+ var logEventInfo = new LogEventInfo();
+ logEventInfo.Properties["valueSeparator1"] = ">";
+
+ // Act
+ string result = renderer.Render(logEventInfo);
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Single_Cookie_Flat_Formatting()
+ {
+ var expectedResult = "key=TEST";
+
+ var renderer = CreateRenderer(addSecondCookie: false);
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Single_Cookie_Json_Formatting()
+ {
+ var expectedResult = "[{\"key\":\"TEST\"}]";
+
+ var renderer = CreateRenderer(addSecondCookie: false);
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.JsonArray;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Single_Cookie_Json_Formatting_no_array()
+ {
+ var expectedResult = "{\"key\":\"TEST\"}";
+
+ var renderer = CreateRenderer(addSecondCookie: false);
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.JsonDictionary;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Multiple_Cookies_Json_Formatting()
+ {
+ var expectedResult = "[{\"key\":\"TEST\"},{\"Key1\":\"TEST1\"}]";
+
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.JsonArray;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Multiple_Cookies_Json_Formatting_no_array()
+ {
+ var expectedResult = "{\"key\":\"TEST\",\"Key1\":\"TEST1\"}";
+
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.JsonDictionary;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyNotFoundRendersEmptyString_Flat_Formatting_ValuesOnly()
+ {
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.Flat;
+ renderer.CookieNames = new List { "notfound" };
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void KeyNotFoundRendersEmptyString_Json_Formatting_ValuesOnly()
+ {
+ var renderer = CreateRenderer();
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.JsonArray;
+ renderer.CookieNames = new List { "notfound" };
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Empty(result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Cookie_Multiple_Items_Flat_Formatting_ValuesOnly()
+ {
+ var expectedResult = "TEST,TEST1";
+
+ var renderer = CreateRenderer();
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Cookie_Multiple_Items_Flat_Formatting_separators_ValuesOnly()
+ {
+ var expectedResult = "TEST|TEST1";
+
+ var renderer = CreateRenderer();
+ renderer.ValueSeparator = ":";
+ renderer.ItemSeparator = "|";
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Single_Item_Flat_Formatting_ValuesOnly()
+ {
+ var expectedResult = "TEST";
+
+ var renderer = CreateRenderer(addSecondCookie: false);
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [InlineData(AspNetRequestLayoutOutputFormat.JsonArray)]
+ [InlineData(AspNetRequestLayoutOutputFormat.JsonDictionary)]
+ public void KeyFoundRendersValue_Single_Item_JsonArray_Formatting_ValuesOnly(AspNetRequestLayoutOutputFormat outputFormat)
+ {
+ var expectedResult = "[\"TEST\"]";
+
+ var renderer = CreateRenderer(addSecondCookie: false);
+ renderer.OutputFormat = outputFormat;
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Theory]
+ [InlineData(AspNetRequestLayoutOutputFormat.JsonArray)]
+ [InlineData(AspNetRequestLayoutOutputFormat.JsonDictionary)]
+ public void KeyFoundRendersValue_Multiple_Items_Json_Formatting_ValuesOnly(AspNetRequestLayoutOutputFormat outputFormat)
+ {
+ var expectedResult = "[\"TEST\",\"TEST1\"]";
+
+ var renderer = CreateRenderer();
+
+ renderer.OutputFormat = outputFormat;
+ renderer.ValuesOnly = true;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ //no multivalue cookie keys in ASP.NET core
+#if !ASP_NET_CORE
+
+ [Fact]
+ public void KeyFoundRendersValue_Multiple_Cookies_And_Cookie_Values_Flat_Formatting()
+ {
+ var expectedResult = "key=TEST,key2=Test&key3=Test456";
+
+ var renderer = CreateRenderer(addSecondCookie: true, addMultiValueCookieKey: true);
+ renderer.CookieNames = new List { "key", "key2" };
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void KeyFoundRendersValue_Multiple_Cookies_And_Cookie_Values_Json_Formatting()
+ {
+ var expectedResult = "[{\"key\":\"TEST\"},{\"Key1\":\"TEST1\"},{\"key2\":\"Test\"},{\"key3\":\"Test456\"}]";
+ var renderer = CreateRenderer(addSecondCookie: true, addMultiValueCookieKey: true);
+ renderer.OutputFormat = AspNetRequestLayoutOutputFormat.JsonArray;
+
+ string result = renderer.Render(new LogEventInfo());
+
+ Assert.Equal(expectedResult, result);
+ }
+#endif
+
+#if !ASP_NET_CORE //todo
+
+ [Fact]
+ public void CommaSeperatedCookieNamesTest_Multiple_Cookie_Values_Flat_Formatting()
+ {
+ // Arrange
+ var expectedResult = "key=TEST&Key1=TEST1";
+
+ var cookie = new HttpCookie("key", "TEST") { ["Key1"] = "TEST1" };
+
+ var layoutRender = new AspNetRequestCookieLayoutRenderer()
+ {
+ CookieNames = new List { "key", "key1" }
+ };
+
+ var httpContextAccessorMock = CreateHttpContextAccessorMockWithCookie(cookie);
+ layoutRender.HttpContextAccessor = httpContextAccessorMock;
+
+ // Act
+ var result = layoutRender.Render(LogEventInfo.CreateNullEvent());
+
+ // Assert
+ Assert.Equal(expectedResult, result);
+ }
+
+ [Fact]
+ public void CommaSeperatedCookieNamesTest_Multiple_Cookie_Values_Json_Formatting()
+ {
+ var expectedResult = "[{\"key\":\"TEST\"},{\"Key1\":\"TEST1\"}]";
+
+ var cookie = new HttpCookie("key", "TEST") { ["Key1"] = "TEST1" };
+
+ var layoutRender = new AspNetRequestCookieLayoutRenderer()
+ {
+ CookieNames = new List { "key", "key1" },
+ OutputFormat = AspNetRequestLayoutOutputFormat.JsonArray
+ };
+
+ var httpContextAccessorMock = CreateHttpContextAccessorMockWithCookie(cookie);
+ layoutRender.HttpContextAccessor = httpContextAccessorMock;
+
+ var result = layoutRender.Render(LogEventInfo.CreateNullEvent());
+
+ Assert.Equal(expectedResult, result);
+ }
+
+ private static IHttpContextAccessor CreateHttpContextAccessorMockWithCookie(HttpCookie cookie)
+ {
+ var httpCookieCollection = new HttpCookieCollection { cookie };
+ var httpContextAccessorMock = Substitute.For();
+ httpContextAccessorMock.HttpContext.Request.Cookies.Returns(httpCookieCollection);
+ return httpContextAccessorMock;
+ }
+
+#endif
+
+ ///
+ /// Create cookie renderer with mocked HTTP context
+ ///
+ /// Add second cookie
+ /// Make cookie multi-value by adding a second value to it
+ /// Created cookie layout renderer
+ ///
+ /// The parameter allows creation of multi-valued cookies with the same key,
+ /// as provided in the HttpCookie API for backwards compatibility with classic ASP.
+ /// This is not supported in ASP.NET Core. For further details, see:
+ /// https://docs.microsoft.com/en-us/dotnet/api/system.web.httpcookie.item?view=netframework-4.7.1#System_Web_HttpCookie_Item_System_String_
+ /// https://stackoverflow.com/a/43831482/6651
+ /// https://github.com/aspnet/HttpAbstractions/issues/831
+ ///
+ private AspNetResponseCookieLayoutRenderer CreateRenderer(bool addSecondCookie = true, bool addMultiValueCookieKey = false)
+ {
+ var cookieNames = new List();
+#if ASP_NET_CORE
+ var httpContext = HttpContext;
+#else
+ var httpContext = Substitute.For();
+#endif
+
+#if ASP_NET_CORE
+ void AddCookie(string key, string value)
+ {
+ cookieNames.Add(key);
+
+ httpContext.Response.Cookies.Append(key,value);
+ }
+
+ AddCookie("key", "TEST");
+
+ if (addSecondCookie)
+ {
+ AddCookie("Key1", "TEST1");
+ }
+
+ if (addMultiValueCookieKey)
+ {
+ throw new NotSupportedException("Multi-valued cookie keys are not supported in ASP.NET Core");
+ }
+
+#else
+
+ var cookie1 = new HttpCookie("key", "TEST");
+ var cookies = new HttpCookieCollection { cookie1 };
+ cookieNames.Add("key");
+
+ if (addSecondCookie)
+ {
+ var cookie2 = new HttpCookie("Key1", "TEST1");
+ cookies.Add(cookie2);
+ cookieNames.Add("Key1");
+ }
+
+ if (addMultiValueCookieKey)
+ {
+ var multiValueCookie = new HttpCookie("key2", "Test");
+ multiValueCookie["key3"] = "Test456";
+ cookies.Add(multiValueCookie);
+ cookieNames.Add("key2");
+ }
+
+ httpContext.Response.Cookies.Returns(cookies);
+#endif
+
+ var renderer = new AspNetResponseCookieLayoutRenderer();
+ renderer.HttpContextAccessor = new FakeHttpContextAccessor(httpContext);
+ renderer.CookieNames = cookieNames;
+ return renderer;
+ }
+ }
+}