-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1f467bf
commit 5936647
Showing
5 changed files
with
112 additions
and
32 deletions.
There are no files selected for viewing
76 changes: 53 additions & 23 deletions
76
src/BitzArt.Blazor.Cookies.Server/Services/HttpContextCookieService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,65 +1,95 @@ | ||
using Microsoft.AspNetCore.DataProtection.KeyManagement; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Http.Features; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace BitzArt.Blazor.Cookies; | ||
|
||
internal class HttpContextCookieService : ICookieService | ||
{ | ||
private readonly HttpContext _httpContext; | ||
private readonly Dictionary<string, Cookie> _cache; | ||
private readonly Dictionary<string, Cookie> _requestCookies; | ||
|
||
public HttpContextCookieService(IHttpContextAccessor httpContextAccessor) | ||
private readonly ILogger _logger; | ||
|
||
private IHeaderDictionary _responseHeaders { get; set; } | ||
|
||
public HttpContextCookieService(IHttpContextAccessor httpContextAccessor, IFeatureCollection features, ILogger<ICookieService> logger) | ||
{ | ||
_httpContext = httpContextAccessor.HttpContext!; | ||
_cache = _httpContext.Request.Cookies | ||
_logger = logger; | ||
|
||
_requestCookies = _httpContext.Request.Cookies | ||
.Select(x => new Cookie(x.Key, x.Value)).ToDictionary(cookie => cookie.Key); | ||
|
||
_responseHeaders = features.GetRequiredFeature<IHttpResponseFeature>().Headers; | ||
} | ||
|
||
public Task<IEnumerable<Cookie>> GetAllAsync() | ||
{ | ||
return Task.FromResult(_cache.Select(x => x.Value).ToList().AsEnumerable()); | ||
return Task.FromResult(_requestCookies.Select(x => x.Value).ToList().AsEnumerable()); | ||
} | ||
|
||
public Task<Cookie?> GetAsync(string key) | ||
{ | ||
if (_cache.TryGetValue(key, out var cookie)) return Task.FromResult<Cookie?>(cookie); | ||
if (_requestCookies.TryGetValue(key, out var cookie)) return Task.FromResult<Cookie?>(cookie); | ||
|
||
return Task.FromResult<Cookie?>(null); | ||
} | ||
|
||
public Task RemoveAsync(string key, CancellationToken cancellationToken = default) | ||
{ | ||
if (!_cache.TryGetValue(key, out _)) return Task.CompletedTask; | ||
if (RemovePending(key)) _logger.LogDebug("Pending cookie [{key}] removed.", key); | ||
|
||
_cache.Remove(key); | ||
_httpContext.Response.Cookies.Delete(key); | ||
if (_requestCookies.Remove(key)) | ||
{ | ||
_logger.LogDebug("Removing client browser cookie [{key}] by marking it as expired.", 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) | ||
public Task SetAsync(Cookie cookie, CancellationToken cancellationToken = default) | ||
{ | ||
var alreadyExists = _cache.TryGetValue(cookie.Key, out var existingCookie); | ||
_logger.LogDebug("Setting cookie: '{key}'='{value}'", cookie.Key, cookie.Value); | ||
|
||
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); | ||
} | ||
RemovePending(cookie.Key); | ||
|
||
_cache.Add(cookie.Key, cookie); | ||
_httpContext.Response.Cookies.Append(cookie.Key, cookie.Value, new CookieOptions | ||
{ | ||
Expires = cookie.Expiration, | ||
Path = "/", | ||
}); | ||
|
||
return Task.CompletedTask; | ||
} | ||
|
||
private bool RemovePending(string key) | ||
{ | ||
_logger.LogDebug("Checking for pending cookie: '{key}'", key); | ||
|
||
var cookieValues = _responseHeaders | ||
.SetCookie | ||
.ToList(); | ||
|
||
for (int i = 0; i < cookieValues.Count; i++) | ||
{ | ||
var value = cookieValues[i]; | ||
if (string.IsNullOrWhiteSpace(value)) continue; | ||
if (!value.StartsWith($"{key}=")) continue; | ||
|
||
_logger.LogDebug("Pending cookie [{key}] found, removing...", key); | ||
cookieValues.RemoveAt(i); | ||
_responseHeaders.SetCookie = new([.. cookieValues]); | ||
_logger.LogDebug("Pending cookie [{key}] removed.", key); | ||
|
||
return true; | ||
} | ||
|
||
_logger.LogDebug("No pending cookie found."); | ||
return false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,48 @@ | ||
namespace BitzArt.Blazor.Cookies; | ||
|
||
/// <summary> | ||
/// Allows interacting with browser cookies. | ||
/// </summary> | ||
public interface ICookieService | ||
{ | ||
/// <summary> | ||
/// Retrieves all cookies. | ||
/// </summary> | ||
public Task<IEnumerable<Cookie>> GetAllAsync(); | ||
|
||
/// <summary> | ||
/// Retrieves a cookie by its key. | ||
/// </summary> | ||
/// <param name="key"> The key of the cookie to retrieve. </param> | ||
/// <returns> The requested cookie, or null if it does not exist. </returns> | ||
public Task<Cookie?> GetAsync(string key); | ||
|
||
/// <summary> | ||
/// Removes a cookie by marking it as expired. | ||
/// </summary> | ||
/// <param name="key"> The key of the cookie to remove. </param> | ||
/// <param name="cancellationToken"> Cancellation token. </param> | ||
/// <returns> A task that represents the asynchronous operation. </returns> | ||
public Task RemoveAsync(string key, CancellationToken cancellationToken = default); | ||
public Task SetAsync(string key, string value, DateTimeOffset? expiration, CancellationToken cancellationToken = default); | ||
|
||
/// <inheritdoc cref="SetAsync(Cookie, CancellationToken)"/> | ||
/// <param name="key"> The name of the cookie to set. </param> | ||
/// <param name="value"> The value of the cookie to set. </param> | ||
/// <param name="expiration"> The cookie's expiration date. </param> | ||
/// <param name="cancellationToken"> Cancellation token. </param> | ||
/// <returns> A task that represents the asynchronous operation. </returns> | ||
public Task SetAsync(string key, string value, DateTimeOffset? expiration, CancellationToken cancellationToken = default) | ||
=> SetAsync(new Cookie(key, value, expiration), cancellationToken); | ||
|
||
/// <summary> | ||
/// Adds or updates a browser cookie. <br/> <br/> | ||
/// When in <see href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes">Static SSR render mode</see>, | ||
/// the new value will be sent to the client machine | ||
/// after the page has completed rendering, | ||
/// and will not appear in the cookies collection until the next request. | ||
/// </summary> | ||
/// <param name="cookie"> The cookie to set. </param> | ||
/// <param name="cancellationToken"> Cancellation token. </param> | ||
/// <returns> A task that represents the asynchronous operation. </returns> | ||
public Task SetAsync(Cookie cookie, CancellationToken cancellationToken = default); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,9 @@ | ||
namespace BitzArt.Blazor.Cookies; | ||
|
||
/// <summary> | ||
/// Browser cookie. | ||
/// </summary> | ||
/// <param name="Key"> The name of the cookie. </param> | ||
/// <param name="Value"> The value of the cookie. </param> | ||
/// <param name="Expiration"> The expiration date of the cookie. </param> | ||
public record Cookie(string Key, string Value, DateTimeOffset? Expiration = null) { } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters