diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..95c11cb
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,26 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            // Use IntelliSense to find out which attributes exist for C# debugging
+            // Use hover for the description of the existing attributes
+            // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
+            "name": ".NET Core Launch (console)",
+            "type": "coreclr",
+            "request": "launch",
+            "preLaunchTask": "build",
+            // If you have changed target frameworks, make sure to update the program path.
+            "program": "${workspaceFolder}/tests/AnonymousUserTests/bin/Debug/net6.0/AnonymousUserTests.dll",
+            "args": [],
+            "cwd": "${workspaceFolder}/tests/AnonymousUserTests",
+            // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
+            "console": "internalConsole",
+            "stopAtEntry": false
+        },
+        {
+            "name": ".NET Core Attach",
+            "type": "coreclr",
+            "request": "attach"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..6a0b954
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,41 @@
+{
+    "version": "2.0.0",
+    "tasks": [
+        {
+            "label": "build",
+            "command": "dotnet",
+            "type": "process",
+            "args": [
+                "build",
+                "${workspaceFolder}/tests/AnonymousUserTests/AnonymousUserTests.csproj",
+                "/property:GenerateFullPaths=true",
+                "/consoleloggerparameters:NoSummary"
+            ],
+            "problemMatcher": "$msCompile"
+        },
+        {
+            "label": "publish",
+            "command": "dotnet",
+            "type": "process",
+            "args": [
+                "publish",
+                "${workspaceFolder}/tests/AnonymousUserTests/AnonymousUserTests.csproj",
+                "/property:GenerateFullPaths=true",
+                "/consoleloggerparameters:NoSummary"
+            ],
+            "problemMatcher": "$msCompile"
+        },
+        {
+            "label": "watch",
+            "command": "dotnet",
+            "type": "process",
+            "args": [
+                "watch",
+                "run",
+                "--project",
+                "${workspaceFolder}/tests/AnonymousUserTests/AnonymousUserTests.csproj"
+            ],
+            "problemMatcher": "$msCompile"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/AspNetCoreExtensions.sln b/AspNetCoreExtensions.sln
index a2b6e94..81fda1c 100644
--- a/AspNetCoreExtensions.sln
+++ b/AspNetCoreExtensions.sln
@@ -3,6 +3,14 @@ Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio Version 16
 VisualStudioVersion = 16.6.30114.105
 MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{38DD8F4A-2C3A-486C-AE3A-7208007EB1B5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnonymousUser", "src\AnonymousUser\AnonymousUser.csproj", "{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9CEDA0A6-C7E0-4542-896C-241C3D796D89}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnonymousUserTests", "tests\AnonymousUserTests\AnonymousUserTests.csproj", "{82E9B456-9918-4499-A165-A30423BA0740}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -15,4 +23,34 @@ Global
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x64.Build.0 = Debug|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Debug|x86.Build.0 = Debug|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|Any CPU.Build.0 = Release|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x64.ActiveCfg = Release|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x64.Build.0 = Release|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x86.ActiveCfg = Release|Any CPU
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96}.Release|x86.Build.0 = Release|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Debug|x64.Build.0 = Debug|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Debug|x86.Build.0 = Debug|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Release|Any CPU.Build.0 = Release|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Release|x64.ActiveCfg = Release|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Release|x64.Build.0 = Release|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Release|x86.ActiveCfg = Release|Any CPU
+		{82E9B456-9918-4499-A165-A30423BA0740}.Release|x86.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(NestedProjects) = preSolution
+		{9FB11E67-7313-4BC7-9412-3FF4CAF35C96} = {38DD8F4A-2C3A-486C-AE3A-7208007EB1B5}
+		{82E9B456-9918-4499-A165-A30423BA0740} = {9CEDA0A6-C7E0-4542-896C-241C3D796D89}
+	EndGlobalSection
 EndGlobal
diff --git a/CodeAnalysis.Library.ruleset b/CodeAnalysis.Library.ruleset
index 2c05d5e..d549136 100644
--- a/CodeAnalysis.Library.ruleset
+++ b/CodeAnalysis.Library.ruleset
@@ -5,6 +5,7 @@
     <Rule Id="CA1006" Action="None" />
     <Rule Id="CA1303" Action="None" />
     <Rule Id="CA2243" Action="None" />
+    <Rule Id="CA2007" Action="None" />
   </Rules>
   <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
     <Rule Id="SA1101" Action="None" /> <!-- Prefix local calls with this -->
diff --git a/appveyor.yml b/appveyor.yml
index f1a37bf..41ba269 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -3,7 +3,7 @@ max_jobs: 1
 environment:
   CAKE_SETTINGS_SKIPPACKAGEVERSIONCHECK: "true"
 
-image: Visual Studio 2019
+image: Visual Studio 2022
 
 cache:
   - '%LocalAppData%\NuGet\v3-cache'
diff --git a/src/AnonymousUser/AnonymousUser.csproj b/src/AnonymousUser/AnonymousUser.csproj
new file mode 100644
index 0000000..f3b02ee
--- /dev/null
+++ b/src/AnonymousUser/AnonymousUser.csproj
@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.1</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
+  </ItemGroup>
+
+</Project>
\ No newline at end of file
diff --git a/src/AnonymousUser/AnonymousUserExtensions.cs b/src/AnonymousUser/AnonymousUserExtensions.cs
new file mode 100644
index 0000000..e961394
--- /dev/null
+++ b/src/AnonymousUser/AnonymousUserExtensions.cs
@@ -0,0 +1,27 @@
+using System;
+using InsightArchitectures.Extensions.AspNetCore.AnonymousUser;
+
+namespace Microsoft.AspNetCore.Builder
+{
+    /// <summary>
+    /// Extension methods for the ASP NET Core application builder.
+    /// </summary>
+    public static class AnonymousUserExtensions
+    {
+        /// <summary>
+        /// Adds the <see cref="AnonymousUserMiddleware" /> to the middleware pipeline.
+        /// </summary>
+        /// <param name="builder">The application builder object.</param>
+        /// <param name="configure">An action to customise the middleware options.</param>
+        public static IApplicationBuilder UseAnonymousUser(this IApplicationBuilder builder, Action<AnonymousUserOptions> configure = null)
+        {
+            var options = new AnonymousUserOptions();
+
+            configure?.Invoke(options);
+
+            _ = options.ClaimType ?? throw new NullReferenceException($"{nameof(options.ClaimType)} is null. Please provide a claim type name when configuring the middleware.");
+
+            return builder.UseMiddleware<AnonymousUserMiddleware>(options);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/AnonymousUser/AnonymousUserMiddleware.cs b/src/AnonymousUser/AnonymousUserMiddleware.cs
new file mode 100644
index 0000000..0f18b9c
--- /dev/null
+++ b/src/AnonymousUser/AnonymousUserMiddleware.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+
+namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser
+{
+    /// <summary>
+    /// The anonymous user middleware. It either creates a new or reads an existing cookie
+    /// and maps the value to a claim.
+    /// </summary>
+    public class AnonymousUserMiddleware
+    {
+        private RequestDelegate _nextDelegate;
+        private AnonymousUserOptions _options;
+
+        /// <summary>
+        /// Constructor requires the next delegate and options.
+        /// </summary>
+        public AnonymousUserMiddleware(RequestDelegate nextDelegate, AnonymousUserOptions options)
+        {
+            _nextDelegate = nextDelegate ?? throw new ArgumentNullException(nameof(nextDelegate));
+            _options = options ?? throw new ArgumentNullException(nameof(options));
+        }
+
+        private async Task HandleRequestAsync(HttpContext httpContext)
+        {
+            var cookieEncoder = _options.EncoderService ?? throw new ArgumentNullException(nameof(_options.EncoderService), $"{nameof(_options.EncoderService)} is null and should have a valid encoder.");
+            _ = _options.UserIdentifierFactory ?? throw new ArgumentNullException(nameof(_options.UserIdentifierFactory), $"{nameof(_options.UserIdentifierFactory)} is null and should have a valid factory.");
+
+            if (httpContext.User.Identity?.IsAuthenticated == true)
+            {
+                return;
+            }
+
+            var encodedValue = httpContext.Request.Cookies[_options.CookieName];
+
+            if (_options.Secure && !httpContext.Request.IsHttps)
+            {
+                if (!string.IsNullOrWhiteSpace(encodedValue))
+                {
+                    httpContext.Response.Cookies.Delete(_options.CookieName);
+                }
+
+                return;
+            }
+
+            var uid = await cookieEncoder.DecodeAsync(encodedValue);
+
+            if (string.IsNullOrWhiteSpace(uid))
+            {
+                uid = _options.UserIdentifierFactory.Invoke(httpContext);
+                var encodedUid = await cookieEncoder.EncodeAsync(uid);
+
+                var cookieOptions = new CookieOptions
+                {
+                    Expires = _options.Expires,
+                };
+
+                httpContext.Response.Cookies.Append(_options.CookieName, encodedUid, cookieOptions);
+            }
+
+            var identity = new ClaimsIdentity(new[] { new Claim(_options.ClaimType, uid) });
+            httpContext.User.AddIdentity(identity);
+        }
+
+        /// <summary>
+        /// Called by the pipeline, runs the handler.
+        /// </summary>
+        public async Task InvokeAsync(HttpContext httpContext)
+        {
+            _ = httpContext ?? throw new ArgumentNullException(nameof(httpContext));
+            await HandleRequestAsync(httpContext);
+
+            await _nextDelegate.Invoke(httpContext);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/AnonymousUser/AnonymousUserOptions.cs b/src/AnonymousUser/AnonymousUserOptions.cs
new file mode 100644
index 0000000..c874aef
--- /dev/null
+++ b/src/AnonymousUser/AnonymousUserOptions.cs
@@ -0,0 +1,29 @@
+using System;
+using Microsoft.AspNetCore.Http;
+
+namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser
+{
+    /// <summary>
+    /// Configuration options for the middleware.
+    /// </summary>
+    public class AnonymousUserOptions
+    {
+        /// <summary>The name of the cookie.</summary>
+        public string CookieName { get; set; } = "tid";
+
+        /// <summary>The expiration date of the cookie. Default set to 10 years.</summary>
+        public DateTimeOffset Expires { get; set; } = DateTimeOffset.UtcNow.AddDays(3652);
+
+        /// <summary>The type name of the claim holding the ID.</summary>
+        public string ClaimType { get; set; } = "ExternalId";
+
+        /// <summary>Should the cookie only be allowed on https requests.</summary>
+        public bool Secure { get; set; }
+
+        /// <summary>Can be overridden to customise the ID generation.</summary>
+        public Func<HttpContext, string> UserIdentifierFactory { get; set; } = _ => Guid.NewGuid().ToString();
+
+        /// <summary>The encoder service to encode/decode the cookie value. Default set to internal base64 encoder.</summary>
+        public ICookieEncoder EncoderService { get; set; } = new Base64CookieEncoder();
+    }
+}
\ No newline at end of file
diff --git a/src/AnonymousUser/Base64CookieEncoder.cs b/src/AnonymousUser/Base64CookieEncoder.cs
new file mode 100644
index 0000000..914f471
--- /dev/null
+++ b/src/AnonymousUser/Base64CookieEncoder.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser
+{
+    /// <summary>
+    /// Default cookie value encoder/decoder. Uses base64 for serialisation.
+    /// </summary>
+    public class Base64CookieEncoder : ICookieEncoder
+    {
+        /// <summary>
+        /// Deserialises a base64 value into clear text.
+        /// <param name="encodedValue">A base64 encoded value.</param>
+        /// <returns>Returns null if <paramref name="encodedValue" /> is null, otherwise the decoded value.</returns>
+        /// </summary>
+        public Task<string> DecodeAsync(string encodedValue)
+        {
+            if (string.IsNullOrWhiteSpace(encodedValue))
+            {
+                return Task.FromResult((string)null);
+            }
+
+            var bytes = Convert.FromBase64String(encodedValue);
+
+            return Task.FromResult(Encoding.UTF8.GetString(bytes));
+        }
+
+        /// <summary>
+        /// Serialiases a clear text value into base64.
+        /// <param name="value">A clear text value.</param>
+        /// <returns>Returns null if <paramref name="value" /> is null, otherwise the encoded value.</returns>
+        /// </summary>
+        public Task<string> EncodeAsync(string value)
+        {
+            if (string.IsNullOrWhiteSpace(value))
+            {
+                return Task.FromResult((string)null);
+            }
+
+            var bytes = Encoding.UTF8.GetBytes(value);
+
+            return Task.FromResult(Convert.ToBase64String(bytes));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/AnonymousUser/ICookieEncoder.cs b/src/AnonymousUser/ICookieEncoder.cs
new file mode 100644
index 0000000..bd192e6
--- /dev/null
+++ b/src/AnonymousUser/ICookieEncoder.cs
@@ -0,0 +1,22 @@
+using System.Threading.Tasks;
+
+namespace InsightArchitectures.Extensions.AspNetCore.AnonymousUser
+{
+    /// <summary>
+    /// Interface for cookie value serialisation.
+    /// </summary>
+    public interface ICookieEncoder
+    {
+        /// <summary>
+        /// Serialiases a clear text value.
+        /// <param name="value">A clear text value</param>
+        /// </summary>
+        Task<string> EncodeAsync(string value);
+
+        /// <summary>
+        /// Deserialises the given value into clear text.
+        /// <param name="encodedValue">The serialised value</param>
+        /// </summary>
+        Task<string> DecodeAsync(string encodedValue);
+    }
+}
\ No newline at end of file
diff --git a/tests/AnonymousUserTests/AnonymousUserMiddlewareTests.cs b/tests/AnonymousUserTests/AnonymousUserMiddlewareTests.cs
new file mode 100644
index 0000000..5d75138
--- /dev/null
+++ b/tests/AnonymousUserTests/AnonymousUserMiddlewareTests.cs
@@ -0,0 +1,90 @@
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using AutoFixture.NUnit3;
+using InsightArchitectures.Extensions.AspNetCore.AnonymousUser;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Internal;
+using Moq;
+using NUnit.Framework;
+
+namespace AnonymousUserTests
+{
+    public class AnonymousUserMiddlewareTests
+    {
+        [Test, CustomAutoDataAttribute]
+        public async Task NoCookiesShouldCreateCookie(HttpContext context, [Frozen] AnonymousUserOptions options, AnonymousUserMiddleware sut)
+        {
+            await sut.InvokeAsync(context);
+
+            var actual = GetCookieValueFromResponse(context.Response, options.CookieName);
+
+            Assert.IsFalse(string.IsNullOrWhiteSpace(actual));
+        }
+
+        [Test, CustomAutoDataAttribute]
+        public async Task ExistingCookieShouldNotAddCookieToResponse(HttpContext context, [Frozen] Mock<HttpRequest> httpRequest, [Frozen] AnonymousUserOptions options, AnonymousUserMiddleware sut)
+        {
+            var cookies = new Dictionary<string, string>
+            {
+                [options.CookieName] = await options.EncoderService.EncodeAsync("RANDOM")
+            };
+            httpRequest.Setup(x => x.Cookies).Returns(new RequestCookieCollection(cookies));
+
+            await sut.InvokeAsync(context);
+
+            var actual = GetCookieValueFromResponse(context.Response, options.CookieName);
+
+            Assert.IsTrue(string.IsNullOrWhiteSpace(actual));
+        }
+
+        [Test, CustomAutoDataAttribute]
+        public async Task SecureCookieWithHttpShouldExpire(HttpContext context, [Frozen] Mock<HttpRequest> httpRequest, [Frozen] AnonymousUserOptions options, AnonymousUserMiddleware sut)
+        {
+            var cookies = new Dictionary<string, string>
+            {
+                [options.CookieName] = await options.EncoderService.EncodeAsync("RANDOM")
+            };
+            httpRequest.Setup(x => x.Cookies).Returns(new RequestCookieCollection(cookies));
+
+            options.Secure = true;
+
+            await sut.InvokeAsync(context);
+
+            var actual = GetCookieValueFromResponse(context.Response, options.CookieName);
+
+            Assert.IsEmpty(actual);
+        }
+
+        [Test, CustomAutoDataAttribute]
+        public async Task AuthenticatedUserShouldSkipMiddleware(HttpContext context, [Frozen] Mock<ClaimsPrincipal> claimsPrincipal, AnonymousUserMiddleware sut)
+        {
+            claimsPrincipal.Setup(x => x.Identity).Returns(new ClaimsIdentity(null, "Test"));
+            claimsPrincipal.Setup(x => x.AddIdentity(It.IsAny<ClaimsIdentity>())).Verifiable();
+
+            await sut.InvokeAsync(context);
+
+            claimsPrincipal.Verify(x => x.AddIdentity(It.IsAny<ClaimsIdentity>()), Times.Never);
+        }
+
+        private string? GetCookieValueFromResponse(HttpResponse response, string cookieName)
+        {
+            foreach (var headers in response.Headers.Values)
+            {
+                foreach (var header in headers)
+                {
+                    if (header.StartsWith(cookieName))
+                    {
+                        {
+                            var p1 = header.IndexOf('=');
+                            var p2 = header.IndexOf(';');
+                            return header.Substring(p1 + 1, p2 - p1 - 1);
+                        }
+                    }
+                }
+            }
+
+            return null;
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/AnonymousUserTests/AnonymousUserTests.csproj b/tests/AnonymousUserTests/AnonymousUserTests.csproj
new file mode 100644
index 0000000..04d51fb
--- /dev/null
+++ b/tests/AnonymousUserTests/AnonymousUserTests.csproj
@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFrameworks>netcoreapp3.1;net5.0;net6.0</TargetFrameworks>
+    <Nullable>enable</Nullable>
+
+    <IsPackable>false</IsPackable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="AutoFixture" Version="4.17.0" />
+    <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" />
+    <PackageReference Include="AutoFixture.NUnit3" Version="4.17.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
+    <PackageReference Include="NUnit" Version="3.13.2" />
+    <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
+    <PackageReference Include="coverlet.collector" Version="3.1.0" />
+    <PackageReference Include="System.ComponentModel.TypeConverter" Version="4.3.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\AnonymousUser\AnonymousUser.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/tests/AnonymousUserTests/Base64CookieEncoderTests.cs b/tests/AnonymousUserTests/Base64CookieEncoderTests.cs
new file mode 100644
index 0000000..e6e568b
--- /dev/null
+++ b/tests/AnonymousUserTests/Base64CookieEncoderTests.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+using InsightArchitectures.Extensions.AspNetCore.AnonymousUser;
+using NUnit.Framework;
+
+namespace AnonymousUserTests
+{
+    public class Base64CookieEncoderTests
+    {
+        [Test, CustomAutoDataAttribute]
+        public async Task EncodingAndDecodingShouldReturnInitialValue(Base64CookieEncoder sut, string initialValue)
+        {
+            var encodedValue = await sut.EncodeAsync(initialValue);
+            var decodedValue = await sut.DecodeAsync(encodedValue);
+
+            Assert.AreEqual(initialValue, decodedValue);
+        }
+
+        [Test, CustomAutoDataAttribute]
+        public async Task EncodingShouldReturnBase64String(Base64CookieEncoder sut, string decodedValue)
+        {
+            var encodedValue = await sut.EncodeAsync(decodedValue);
+
+            Assert.IsTrue(IsBase64String(encodedValue));
+
+            bool IsBase64String(string value)
+            {
+                Span<byte> buffer = stackalloc byte[value.Length];
+                return Convert.TryFromBase64String(value, buffer, out _);
+            }
+        }
+
+        [Test, CustomAutoDataAttribute]
+        public async Task NullInputShouldReturnNull(Base64CookieEncoder sut)
+        {
+            Assert.IsNull(await sut.EncodeAsync(null));
+            Assert.IsNull(await sut.DecodeAsync(null));
+        }
+    }
+}
\ No newline at end of file
diff --git a/tests/AnonymousUserTests/CustomAutoDataAttribute.cs b/tests/AnonymousUserTests/CustomAutoDataAttribute.cs
new file mode 100644
index 0000000..3246090
--- /dev/null
+++ b/tests/AnonymousUserTests/CustomAutoDataAttribute.cs
@@ -0,0 +1,59 @@
+using System;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using AutoFixture.NUnit3;
+using InsightArchitectures.Extensions.AspNetCore.AnonymousUser;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Internal;
+using Moq;
+
+namespace AnonymousUserTests
+{
+
+    [AttributeUsage(AttributeTargets.Method)]
+    public class CustomAutoDataAttribute : AutoDataAttribute
+    {
+        public CustomAutoDataAttribute() : base(FixtureHelpers.CreateFixture)
+        {
+        }
+    }
+
+    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
+    public class CustomInlineAutoDataAttribute : InlineAutoDataAttribute
+    {
+        public CustomInlineAutoDataAttribute(params object[] args) : base(FixtureHelpers.CreateFixture, args)
+        {
+        }
+    }
+
+    internal static class FixtureHelpers
+    {
+        public static IFixture CreateFixture()
+        {
+            var fixture = new Fixture();
+
+            fixture.Customize(new AutoMoqCustomization
+            {
+                ConfigureMembers = true,
+                GenerateDelegates = true,
+            });
+
+            fixture.Customize<Mock<HttpRequest>>(sb => sb.FromFactory(() => new Mock<HttpRequest>()));
+
+            var requestMock = fixture.Freeze<Mock<HttpRequest>>();
+            requestMock.Setup(x => x.Cookies).Returns(new RequestCookieCollection());
+            requestMock.Setup(x => x.IsHttps).Returns(false);
+
+            var contextMock = fixture.Freeze<Mock<HttpContext>>();
+            contextMock.Setup(x => x.Response).Returns(new DefaultHttpResponse(new DefaultHttpContext()));
+
+            fixture.Customize<AnonymousUserOptions>(sb =>
+            {
+                return sb.With(x => x.EncoderService, new Base64CookieEncoder())
+                        .With(x => x.Secure, false);
+            });
+
+            return fixture;
+        }
+    }
+}
\ No newline at end of file