-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Expand file tree
/
Copy pathDistributedCacheService.cs
More file actions
146 lines (127 loc) · 5.32 KB
/
DistributedCacheService.cs
File metadata and controls
146 lines (127 loc) · 5.32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Text;
using System.Text.Json;
namespace FSH.Framework.Caching;
/// <summary>
/// Implementation of <see cref="ICacheService"/> using distributed cache (Redis or in-memory).
/// Provides JSON serialization for cached objects with configurable expiration policies.
/// </summary>
public sealed class DistributedCacheService : ICacheService
{
private static readonly Encoding Utf8 = Encoding.UTF8;
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
private readonly IDistributedCache _cache;
private readonly ILogger<DistributedCacheService> _logger;
private readonly CachingOptions _opts;
/// <summary>
/// Initializes a new instance of <see cref="DistributedCacheService"/>.
/// </summary>
/// <param name="cache">The underlying distributed cache implementation.</param>
/// <param name="logger">Logger for cache operations.</param>
/// <param name="opts">Caching configuration options.</param>
public DistributedCacheService(
IDistributedCache cache,
ILogger<DistributedCacheService> logger,
IOptions<CachingOptions> opts)
{
ArgumentNullException.ThrowIfNull(opts);
_cache = cache;
_logger = logger;
_opts = opts.Value;
}
/// <inheritdoc />
public async Task<T?> GetItemAsync<T>(string key, CancellationToken ct = default)
{
key = Normalize(key);
try
{
var bytes = await _cache.GetAsync(key, ct).ConfigureAwait(false);
if (bytes is null || bytes.Length == 0) return default;
return JsonSerializer.Deserialize<T>(Utf8.GetString(bytes), JsonOpts);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "Cache get failed for {Key}", key);
return default;
}
}
/// <inheritdoc />
public async Task SetItemAsync<T>(string key, T value, TimeSpan? sliding = default, CancellationToken ct = default)
{
key = Normalize(key);
try
{
var bytes = Utf8.GetBytes(JsonSerializer.Serialize(value, JsonOpts));
await _cache.SetAsync(key, bytes, BuildEntryOptions(sliding), ct).ConfigureAwait(false);
_logger.LogDebug("Cached {Key}", key);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
_logger.LogWarning(ex, "Cache set failed for {Key}", key);
}
}
/// <inheritdoc />
public async Task RemoveItemAsync(string key, CancellationToken ct = default)
{
key = Normalize(key);
try { await _cache.RemoveAsync(key, ct).ConfigureAwait(false); }
catch (Exception ex) when (ex is not OperationCanceledException)
{ _logger.LogWarning(ex, "Cache remove failed for {Key}", key); }
}
/// <inheritdoc />
public async Task RefreshItemAsync(string key, CancellationToken ct = default)
{
key = Normalize(key);
try
{
await _cache.RefreshAsync(key, ct).ConfigureAwait(false);
_logger.LogDebug("Refreshed {Key}", key);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{ _logger.LogWarning(ex, "Cache refresh failed for {Key}", key); }
}
/// <inheritdoc />
public T? GetItem<T>(string key) => GetItemAsync<T>(key).GetAwaiter().GetResult();
/// <inheritdoc />
public void SetItem<T>(string key, T value, TimeSpan? sliding = default) => SetItemAsync(key, value, sliding).GetAwaiter().GetResult();
/// <inheritdoc />
public void RemoveItem(string key) => RemoveItemAsync(key).GetAwaiter().GetResult();
/// <inheritdoc />
public void RefreshItem(string key) => RefreshItemAsync(key).GetAwaiter().GetResult();
/// <summary>
/// Builds cache entry options with configured expiration settings.
/// </summary>
/// <param name="sliding">Optional sliding expiration override.</param>
/// <returns>Configured cache entry options.</returns>
private DistributedCacheEntryOptions BuildEntryOptions(TimeSpan? sliding)
{
var o = new DistributedCacheEntryOptions();
if (sliding.HasValue)
o.SetSlidingExpiration(sliding.Value);
else if (_opts.DefaultSlidingExpiration.HasValue)
o.SetSlidingExpiration(_opts.DefaultSlidingExpiration.Value);
if (_opts.DefaultAbsoluteExpiration.HasValue)
o.SetAbsoluteExpiration(_opts.DefaultAbsoluteExpiration.Value);
return o;
}
/// <summary>
/// Normalizes the cache key by applying the configured prefix.
/// </summary>
/// <param name="key">The original cache key.</param>
/// <returns>The normalized key with prefix applied.</returns>
/// <exception cref="ArgumentNullException">Thrown when key is null or whitespace.</exception>
private string Normalize(string key)
{
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
var prefix = _opts.KeyPrefix ?? string.Empty;
if (prefix.Length == 0)
{
return key;
}
return key.StartsWith(prefix, StringComparison.Ordinal)
? key
: prefix + key;
}
}