diff --git a/Blazor.Cookies.sln b/Blazor.Cookies.sln
index 112d102..cbbcb8d 100644
--- a/Blazor.Cookies.sln
+++ b/Blazor.Cookies.sln
@@ -25,9 +25,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\Tests.yml = .github\workflows\Tests.yml
EndProjectSection
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitzArt.Blazor.Cookies.Client", "src\BitzArt.Blazor.Cookies.Client\BitzArt.Blazor.Cookies.Client.csproj", "{5E61195E-5AB8-469E-B848-CDB0228F3984}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Blazor.Cookies.Client", "src\BitzArt.Blazor.Cookies.Client\BitzArt.Blazor.Cookies.Client.csproj", "{5E61195E-5AB8-469E-B848-CDB0228F3984}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitzArt.Blazor.Cookies.Server", "src\BitzArt.Blazor.Cookies.Server\BitzArt.Blazor.Cookies.Server.csproj", "{9DDC9769-C0C6-452C-97E1-A11991976106}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BitzArt.Blazor.Cookies.Server", "src\BitzArt.Blazor.Cookies.Server\BitzArt.Blazor.Cookies.Server.csproj", "{9DDC9769-C0C6-452C-97E1-A11991976106}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BitzArt.Blazor.Cookies.Server.Tests", "tests\BitzArt.Blazor.Cookies.Server.Tests\BitzArt.Blazor.Cookies.Server.Tests.csproj", "{117F8E5A-B3AB-4C0C-824C-656F0DD2AB15}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -59,6 +61,10 @@ Global
{9DDC9769-C0C6-452C-97E1-A11991976106}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9DDC9769-C0C6-452C-97E1-A11991976106}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9DDC9769-C0C6-452C-97E1-A11991976106}.Release|Any CPU.Build.0 = Release|Any CPU
+ {117F8E5A-B3AB-4C0C-824C-656F0DD2AB15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {117F8E5A-B3AB-4C0C-824C-656F0DD2AB15}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {117F8E5A-B3AB-4C0C-824C-656F0DD2AB15}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {117F8E5A-B3AB-4C0C-824C-656F0DD2AB15}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -71,6 +77,7 @@ Global
{890E8E99-1FCA-4A48-8189-92FF199211D8} = {53C2EA4F-8EA8-41FE-A091-D85B0314B1F7}
{5E61195E-5AB8-469E-B848-CDB0228F3984} = {F0AC2847-ADDF-4D66-B1FA-2D6B34F206CF}
{9DDC9769-C0C6-452C-97E1-A11991976106} = {F0AC2847-ADDF-4D66-B1FA-2D6B34F206CF}
+ {117F8E5A-B3AB-4C0C-824C-656F0DD2AB15} = {FE7AAF4D-63E3-41CA-8E4F-D16CC839D8DA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {43738754-874B-41F7-8B6C-087023E2CD94}
diff --git a/src/BitzArt.Blazor.Cookies.Server/BitzArt.Blazor.Cookies.Server.csproj b/src/BitzArt.Blazor.Cookies.Server/BitzArt.Blazor.Cookies.Server.csproj
index 4fd35f9..c380389 100644
--- a/src/BitzArt.Blazor.Cookies.Server/BitzArt.Blazor.Cookies.Server.csproj
+++ b/src/BitzArt.Blazor.Cookies.Server/BitzArt.Blazor.Cookies.Server.csproj
@@ -27,4 +27,10 @@
+
+
+ <_Parameter1>BitzArt.Blazor.Cookies.Server.Tests
+
+
+
diff --git a/src/BitzArt.Blazor.Cookies.Server/Services/HttpContextCookieService.cs b/src/BitzArt.Blazor.Cookies.Server/Services/HttpContextCookieService.cs
index 21b8a18..4119967 100644
--- a/src/BitzArt.Blazor.Cookies.Server/Services/HttpContextCookieService.cs
+++ b/src/BitzArt.Blazor.Cookies.Server/Services/HttpContextCookieService.cs
@@ -1,51 +1,65 @@
-using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.DataProtection.KeyManagement;
+using Microsoft.AspNetCore.Http;
namespace BitzArt.Blazor.Cookies;
internal class HttpContextCookieService : ICookieService
{
private readonly HttpContext _httpContext;
- private readonly List _cache;
+ private readonly Dictionary _cache;
public HttpContextCookieService(IHttpContextAccessor httpContextAccessor)
{
_httpContext = httpContextAccessor.HttpContext!;
_cache = _httpContext.Request.Cookies
- .Select(x => new Cookie(x.Key, x.Value)).ToList();
+ .Select(x => new Cookie(x.Key, x.Value)).ToDictionary(cookie => cookie.Key);
}
public Task> GetAllAsync()
{
- return Task.FromResult(_cache.ToList().AsEnumerable());
+ return Task.FromResult(_cache.Select(x => x.Value).ToList().AsEnumerable());
}
public Task GetAsync(string key)
{
- return Task.FromResult(_cache.FirstOrDefault(x => x.Key == key));
+ if (_cache.TryGetValue(key, out var cookie)) return Task.FromResult(cookie);
+
+ return Task.FromResult(null);
}
public Task RemoveAsync(string key, CancellationToken cancellationToken = default)
{
- var cookie = _cache.FirstOrDefault(x => x.Key == key);
- if (cookie is null) return Task.CompletedTask;
+ if (!_cache.TryGetValue(key, out _)) return Task.CompletedTask;
- _cache.Remove(cookie);
+ _cache.Remove(key);
_httpContext.Response.Cookies.Delete(key);
return Task.CompletedTask;
}
public Task SetAsync(string key, string value, DateTimeOffset? expiration, CancellationToken cancellationToken = default)
+ => SetAsync(new Cookie(key, value, expiration), cancellationToken);
+
+ public async Task SetAsync(Cookie cookie, CancellationToken cancellationToken = default)
{
- _cache.Add(new Cookie(key, value, expiration));
- _httpContext.Response.Cookies.Append(key, value, new CookieOptions
+ var alreadyExists = _cache.TryGetValue(cookie.Key, out var existingCookie);
+
+ if (alreadyExists)
+ {
+ // If the cookie already exists and the value has not changed,
+ // we don't need to update it.
+ if (existingCookie == cookie) return;
+
+ // If the cookie already exists and the new value has changed,
+ // we remove the old one before adding the new one.
+ await RemoveAsync(cookie.Key, cancellationToken);
+ }
+
+ _cache.Add(cookie.Key, cookie);
+ _httpContext.Response.Cookies.Append(cookie.Key, cookie.Value, new CookieOptions
{
- Expires = expiration,
+ Expires = cookie.Expiration,
Path = "/",
});
- return Task.CompletedTask;
}
-
- public Task SetAsync(Cookie cookie, CancellationToken cancellationToken = default)
- => SetAsync(cookie.Key, cookie.Value, cookie.Expiration, cancellationToken);
}
diff --git a/src/BitzArt.Blazor.Cookies/Model/Cookie.cs b/src/BitzArt.Blazor.Cookies/Model/Cookie.cs
index cf39168..8093bfc 100644
--- a/src/BitzArt.Blazor.Cookies/Model/Cookie.cs
+++ b/src/BitzArt.Blazor.Cookies/Model/Cookie.cs
@@ -1,15 +1,3 @@
namespace BitzArt.Blazor.Cookies;
-public class Cookie
-{
- public string Key { get; set; }
- public string Value { get; set; }
- public DateTimeOffset? Expiration { get; set; }
-
- public Cookie(string key, string value, DateTimeOffset? expiration = null)
- {
- Key = key;
- Value = value;
- Expiration = expiration;
- }
-}
+public record Cookie(string Key, string Value, DateTimeOffset? Expiration = null) { }
diff --git a/src/BitzArt.Blazor.Cookies/Services/BrowserCookieService.cs b/src/BitzArt.Blazor.Cookies/Services/BrowserCookieService.cs
index f0e2942..20254d5 100644
--- a/src/BitzArt.Blazor.Cookies/Services/BrowserCookieService.cs
+++ b/src/BitzArt.Blazor.Cookies/Services/BrowserCookieService.cs
@@ -7,7 +7,7 @@ internal class BrowserCookieService(IJSRuntime js) : ICookieService
public async Task> GetAllAsync()
{
var raw = await js.InvokeAsync("eval", "document.cookie");
- if (string.IsNullOrWhiteSpace(raw)) return Enumerable.Empty();
+ if (string.IsNullOrWhiteSpace(raw)) return [];
return raw.Split("; ").Select(x =>
{
diff --git a/tests/BitzArt.Blazor.Cookies.Server.Tests/BitzArt.Blazor.Cookies.Server.Tests.csproj b/tests/BitzArt.Blazor.Cookies.Server.Tests/BitzArt.Blazor.Cookies.Server.Tests.csproj
new file mode 100644
index 0000000..939cf93
--- /dev/null
+++ b/tests/BitzArt.Blazor.Cookies.Server.Tests/BitzArt.Blazor.Cookies.Server.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/BitzArt.Blazor.Cookies.Server.Tests/HttpContextCookieServiceTests.cs b/tests/BitzArt.Blazor.Cookies.Server.Tests/HttpContextCookieServiceTests.cs
new file mode 100644
index 0000000..362b63d
--- /dev/null
+++ b/tests/BitzArt.Blazor.Cookies.Server.Tests/HttpContextCookieServiceTests.cs
@@ -0,0 +1,75 @@
+using Microsoft.AspNetCore.Http;
+
+namespace BitzArt.Blazor.Cookies.Server.Tests;
+
+public class HttpContextCookieServiceTests
+{
+ [Fact]
+ public async Task SetCookie_WhenProperCookie_ShouldSetCookie()
+ {
+ // Arrange
+ (var httpContext, _, var service) = CreateTestServices();
+
+ // Act
+ await service.SetAsync("key", "value", null);
+
+ // Assert
+ Assert.Single(httpContext.Response.Headers);
+ Assert.Single(httpContext.Response.Headers["Set-Cookie"]);
+ Assert.Contains("key=value", httpContext.Response.Headers["Set-Cookie"].First());
+ }
+
+ [Fact]
+ public async Task RemoveCookie_AfterSetCookie_ShouldRemoveCookie()
+ {
+ // Arrange
+ (var httpContext, _, var service) = CreateTestServices();
+
+ await service.SetAsync("key", "value", null);
+
+ // Act
+ await service.RemoveAsync("key");
+
+ // Assert
+ Assert.Single(httpContext.Response.Headers);
+ Assert.Single(httpContext.Response.Headers["Set-Cookie"]);
+ Assert.Contains("key=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=", httpContext.Response.Headers["Set-Cookie"].First());
+ }
+
+ [Fact]
+ public async Task SetCookie_WhenDuplicate_ShouldOnlySetCookieOnce()
+ {
+ // Arrange
+ (var httpContext, _, var service) = CreateTestServices();
+
+ // Act
+ await service.SetAsync("key", "value", null);
+ await service.SetAsync("key", "value", null);
+
+ // Assert
+ Assert.Single(httpContext.Response.Headers);
+ }
+
+ private static TestServices CreateTestServices()
+ {
+ var httpContext = new DefaultHttpContext();
+ var accessor = new TestHttpContextAccessor(httpContext);
+
+ var cookieService = new HttpContextCookieService(accessor);
+
+ return new TestServices(httpContext, accessor, cookieService);
+ }
+
+ private record TestServices(HttpContext HttpContext, IHttpContextAccessor HttpContextAccessor, ICookieService CookieService);
+
+ private class TestHttpContextAccessor(HttpContext httpContext) : IHttpContextAccessor
+ {
+ private HttpContext? _httpContext = httpContext;
+
+ public HttpContext? HttpContext
+ {
+ get => _httpContext;
+ set => _httpContext = value;
+ }
+ }
+}
\ No newline at end of file