Skip to content

Commit 26dda32

Browse files
committed
Helper method
1 parent 2453f70 commit 26dda32

File tree

2 files changed

+69
-27
lines changed

2 files changed

+69
-27
lines changed

DigitalRuby.SimpleCache/DigitalRuby.SimpleCache.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
88
<GenerateDocumentationFile>true</GenerateDocumentationFile>
99
<IsPackable>true</IsPackable>
10-
<Version>3.0.1</Version>
10+
<Version>3.0.2</Version>
1111
<Title>Simple Cache</Title>
1212
<Authors>jjxtra</Authors>
1313
<Company>Digital Ruby, LLC</Company>

DigitalRuby.SimpleCache/FileCache.cs

+68-26
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,18 @@ public interface IFileCache
7979
/// </summary>
8080
public ISerializer Serializer { get; }
8181

82+
/// <summary>
83+
/// Time provider
84+
/// </summary>
85+
public TimeProvider Clock { get; }
86+
8287
/// <summary>
8388
/// Get a cache value
8489
/// </summary>
8590
/// <typeparam name="T">Type of value</typeparam>
8691
/// <param name="key">Key</param>
8792
/// <param name="cancelToken">Cancel token</param>
88-
/// <returns>Task of type T, T will be null if not found</returns>
93+
/// <returns>Task of FileCacheItem of type T, null if not found</returns>
8994
/// <exception cref="NullReferenceException">Key is null</exception>
9095
Task<FileCacheItem<T>?> GetAsync<T>(string key, CancellationToken cancelToken = default);
9196

@@ -101,15 +106,39 @@ public interface IFileCache
101106
/// <exception cref="NullReferenceException">Key is null</exception>
102107
Task SetAsync(string key, object value, CacheParameters cacheParameters = default, CancellationToken cancelToken = default);
103108

104-
/// <summary>
105-
/// Remove an item from the cache
106-
/// </summary>
107-
/// <param name="key">Key</param>
108-
/// <param name="cancelToken">Cancel token</param>
109-
/// <returns>Task</returns>
110-
/// <exception cref="NullReferenceException">Key is null</exception>
111-
/// <remarks>Ensure the the type parameter is the exact type that you used to add the item to the cache</remarks>
112-
Task RemoveAsync(string key, CancellationToken cancelToken = default);
109+
/// <summary>
110+
/// Get or create a cache item if it doesn't exist
111+
/// </summary>
112+
/// <param name="key">Key</param>
113+
/// <param name="valueFactory">Value factory</param>
114+
/// <param name="cacheParameters">Cache parameters</param>
115+
/// <param name="cancelToken">Cancel token</param>
116+
/// <returns>Task of file cache item of type T</returns>
117+
/// <exception cref="NullReferenceException">Key is null</exception>
118+
async Task<FileCacheItem<T>> GetOrCreateAsync<T>(string key, Func<CancellationToken, Task<T>> valueFactory, CacheParameters cacheParameters = default, CancellationToken cancelToken = default)
119+
{
120+
var result = await GetAsync<T>(key, cancelToken);
121+
if (result is null)
122+
{
123+
var value = await valueFactory(cancelToken);
124+
if (value is not null && value is not Exception && value is not Task)
125+
{
126+
await SetAsync(key, value, cacheParameters, cancelToken);
127+
}
128+
result = new FileCacheItem<T>(Clock.GetUtcNow() + cacheParameters.Duration, value, cacheParameters.Size);
129+
}
130+
return result;
131+
}
132+
133+
/// <summary>
134+
/// Remove an item from the cache
135+
/// </summary>
136+
/// <param name="key">Key</param>
137+
/// <param name="cancelToken">Cancel token</param>
138+
/// <returns>Task</returns>
139+
/// <exception cref="NullReferenceException">Key is null</exception>
140+
/// <remarks>Ensure the the type parameter is the exact type that you used to add the item to the cache</remarks>
141+
Task RemoveAsync(string key, CancellationToken cancelToken = default);
113142

114143
/// <summary>
115144
/// Remove all cache items
@@ -128,14 +157,19 @@ public sealed class NullFileCache : IFileCache
128157
/// </summary>
129158
public ISerializer Serializer { get; } = new JsonLZ4Serializer();
130159

131-
/// <inheritdoc />
160+
/// <summary>
161+
/// Clock
162+
/// </summary>
163+
public TimeProvider Clock { get; } = TimeProvider.System;
164+
165+
/// <inheritdoc />
132166
public Task<FileCacheItem<T>?> GetAsync<T>(string key, CancellationToken cancelToken = default)
133167
{
134168
return Task.FromResult<FileCacheItem<T>?>(null);
135169
}
136170

137-
/// <inheritdoc />
138-
public Task RemoveAsync(string key, CancellationToken cancelToken = default)
171+
/// <inheritdoc />
172+
public Task RemoveAsync(string key, CancellationToken cancelToken = default)
139173
{
140174
return Task.CompletedTask;
141175
}
@@ -161,12 +195,16 @@ public Task SetAsync(string key, object value, CacheParameters cacheParameters =
161195
public sealed class MemoryFileCache(ISerializer serializer, TimeProvider clock) : IFileCache
162196
{
163197
private readonly ISerializer serializer = serializer;
164-
private readonly TimeProvider clock = clock;
165198
private readonly ConcurrentDictionary<string, FileCacheItem<byte[]>> items = new();
166199

167200
/// <inheritdoc />
168201
public ISerializer Serializer => serializer;
169202

203+
/// <summary>
204+
/// Clock
205+
/// </summary>
206+
public TimeProvider Clock { get; } = TimeProvider.System;
207+
170208
/// <inheritdoc />
171209
public Task<FileCacheItem<T>?> GetAsync<T>(string key, CancellationToken cancelToken = default)
172210
{
@@ -182,8 +220,8 @@ public sealed class MemoryFileCache(ISerializer serializer, TimeProvider clock)
182220
return Task.FromResult<FileCacheItem<T>?>(null);
183221
}
184222

185-
/// <inheritdoc />
186-
public Task RemoveAsync(string key, CancellationToken cancelToken = default)
223+
/// <inheritdoc />
224+
public Task RemoveAsync(string key, CancellationToken cancelToken = default)
187225
{
188226
items.TryRemove(key, out _);
189227
return Task.CompletedTask;
@@ -208,13 +246,17 @@ public Task ClearAsync()
208246
/// <inheritdoc />
209247
public sealed class FileCache : BackgroundService, IFileCache
210248
{
249+
/// <summary>
250+
/// Clock
251+
/// </summary>
252+
public TimeProvider Clock { get; }
253+
211254
private static readonly Type byteArrayType = typeof(byte[]);
212255
private static readonly TimeSpan cleanupLoopDelay = TimeSpan.FromMilliseconds(1.0);
213256

214257
private readonly MultithreadedKeyLocker keyLocker = new(512);
215258

216259
private readonly IDiskSpace diskSpace;
217-
private readonly TimeProvider clock;
218260
private readonly ILogger logger;
219261

220262
private readonly string baseDir;
@@ -256,7 +298,7 @@ public FileCache(FileCacheOptions options,
256298
{
257299
Serializer = serializer;
258300
this.diskSpace = diskSpace;
259-
this.clock = clock;
301+
Clock = clock;
260302
this.logger = logger;
261303
string assemblyName = Assembly.GetEntryAssembly()!.GetName().Name!;
262304
baseDir = options.CacheDirectory;
@@ -300,7 +342,7 @@ public FileCache(FileCacheOptions options,
300342
using BinaryReader reader = new(readerStream);
301343
long ticks = reader.ReadInt64();
302344
DateTimeOffset cutOff = new(ticks, TimeSpan.Zero);
303-
if (clock.GetUtcNow() >= cutOff)
345+
if (Clock.GetUtcNow() >= cutOff)
304346
{
305347
// expired, delete, no exception for performance
306348
logger.LogDebug("File cache expired {key}, {fileName}, deleting", key, fileName);
@@ -325,15 +367,15 @@ public FileCache(FileCacheOptions options,
325367
throw new IOException("Byte counts are off for file cache item");
326368
}
327369
FileCacheItem<T> result;
328-
if (typeof(T) == byteArrayType)
370+
if (typeof(T) == byteArrayType)
329371
{
330-
result = new FileCacheItem<T>(new DateTimeOffset(ticks, TimeSpan.Zero), (T)(object)bytes, size);
331-
}
372+
result = new FileCacheItem<T>(new DateTimeOffset(ticks, TimeSpan.Zero), (T)(object)bytes, size);
373+
}
332374
else
333375
{
334376
var item = (T?)Serializer.Deserialize(bytes, typeof(T?)) ?? throw new IOException("Corrupt cache file " + fileName);
335-
result = new FileCacheItem<T>(new DateTimeOffset(ticks, TimeSpan.Zero), item, size);
336-
}
377+
result = new FileCacheItem<T>(new DateTimeOffset(ticks, TimeSpan.Zero), item, size);
378+
}
337379
logger.LogDebug("File cache hit {key}, {fileName}", key, fileName);
338380
return result;
339381
}
@@ -403,7 +445,7 @@ public async Task SetAsync(string key, object value, CacheParameters cacheParame
403445
{
404446
using FileStream writerStream = new(fileName, FileMode.Create, FileAccess.Write, FileShare.None);
405447
using BinaryWriter writer = new(writerStream);
406-
DateTimeOffset expires = clock.GetUtcNow() + cacheParameters.Duration;
448+
DateTimeOffset expires = Clock.GetUtcNow() + cacheParameters.Duration;
407449
writer.Write(expires.Ticks);
408450
byte[]? bytes = (value is byte[] alreadyBytes ? alreadyBytes : Serializer.Serialize(value));
409451
if (bytes is not null)
@@ -516,4 +558,4 @@ public sealed class FileCacheOptions
516558
/// Percentage (0 - 100) of free space remaining to trigger cleanup of files. Default is 15.
517559
/// </summary>
518560
public int FreeSpaceThreshold { get; set; } = 15;
519-
}
561+
}

0 commit comments

Comments
 (0)