@@ -79,13 +79,18 @@ public interface IFileCache
79
79
/// </summary>
80
80
public ISerializer Serializer { get ; }
81
81
82
+ /// <summary>
83
+ /// Time provider
84
+ /// </summary>
85
+ public TimeProvider Clock { get ; }
86
+
82
87
/// <summary>
83
88
/// Get a cache value
84
89
/// </summary>
85
90
/// <typeparam name="T">Type of value</typeparam>
86
91
/// <param name="key">Key</param>
87
92
/// <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>
89
94
/// <exception cref="NullReferenceException">Key is null</exception>
90
95
Task < FileCacheItem < T > ? > GetAsync < T > ( string key , CancellationToken cancelToken = default ) ;
91
96
@@ -101,15 +106,39 @@ public interface IFileCache
101
106
/// <exception cref="NullReferenceException">Key is null</exception>
102
107
Task SetAsync ( string key , object value , CacheParameters cacheParameters = default , CancellationToken cancelToken = default ) ;
103
108
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 ) ;
113
142
114
143
/// <summary>
115
144
/// Remove all cache items
@@ -128,14 +157,19 @@ public sealed class NullFileCache : IFileCache
128
157
/// </summary>
129
158
public ISerializer Serializer { get ; } = new JsonLZ4Serializer ( ) ;
130
159
131
- /// <inheritdoc />
160
+ /// <summary>
161
+ /// Clock
162
+ /// </summary>
163
+ public TimeProvider Clock { get ; } = TimeProvider . System ;
164
+
165
+ /// <inheritdoc />
132
166
public Task < FileCacheItem < T > ? > GetAsync < T > ( string key , CancellationToken cancelToken = default )
133
167
{
134
168
return Task . FromResult < FileCacheItem < T > ? > ( null ) ;
135
169
}
136
170
137
- /// <inheritdoc />
138
- public Task RemoveAsync ( string key , CancellationToken cancelToken = default )
171
+ /// <inheritdoc />
172
+ public Task RemoveAsync ( string key , CancellationToken cancelToken = default )
139
173
{
140
174
return Task . CompletedTask ;
141
175
}
@@ -161,12 +195,16 @@ public Task SetAsync(string key, object value, CacheParameters cacheParameters =
161
195
public sealed class MemoryFileCache ( ISerializer serializer , TimeProvider clock ) : IFileCache
162
196
{
163
197
private readonly ISerializer serializer = serializer ;
164
- private readonly TimeProvider clock = clock ;
165
198
private readonly ConcurrentDictionary < string , FileCacheItem < byte [ ] > > items = new ( ) ;
166
199
167
200
/// <inheritdoc />
168
201
public ISerializer Serializer => serializer ;
169
202
203
+ /// <summary>
204
+ /// Clock
205
+ /// </summary>
206
+ public TimeProvider Clock { get ; } = TimeProvider . System ;
207
+
170
208
/// <inheritdoc />
171
209
public Task < FileCacheItem < T > ? > GetAsync < T > ( string key , CancellationToken cancelToken = default )
172
210
{
@@ -182,8 +220,8 @@ public sealed class MemoryFileCache(ISerializer serializer, TimeProvider clock)
182
220
return Task . FromResult < FileCacheItem < T > ? > ( null ) ;
183
221
}
184
222
185
- /// <inheritdoc />
186
- public Task RemoveAsync ( string key , CancellationToken cancelToken = default )
223
+ /// <inheritdoc />
224
+ public Task RemoveAsync ( string key , CancellationToken cancelToken = default )
187
225
{
188
226
items . TryRemove ( key , out _ ) ;
189
227
return Task . CompletedTask ;
@@ -208,13 +246,17 @@ public Task ClearAsync()
208
246
/// <inheritdoc />
209
247
public sealed class FileCache : BackgroundService , IFileCache
210
248
{
249
+ /// <summary>
250
+ /// Clock
251
+ /// </summary>
252
+ public TimeProvider Clock { get ; }
253
+
211
254
private static readonly Type byteArrayType = typeof ( byte [ ] ) ;
212
255
private static readonly TimeSpan cleanupLoopDelay = TimeSpan . FromMilliseconds ( 1.0 ) ;
213
256
214
257
private readonly MultithreadedKeyLocker keyLocker = new ( 512 ) ;
215
258
216
259
private readonly IDiskSpace diskSpace ;
217
- private readonly TimeProvider clock ;
218
260
private readonly ILogger logger ;
219
261
220
262
private readonly string baseDir ;
@@ -256,7 +298,7 @@ public FileCache(FileCacheOptions options,
256
298
{
257
299
Serializer = serializer ;
258
300
this . diskSpace = diskSpace ;
259
- this . clock = clock ;
301
+ Clock = clock ;
260
302
this . logger = logger ;
261
303
string assemblyName = Assembly . GetEntryAssembly ( ) ! . GetName ( ) . Name ! ;
262
304
baseDir = options . CacheDirectory ;
@@ -300,7 +342,7 @@ public FileCache(FileCacheOptions options,
300
342
using BinaryReader reader = new ( readerStream ) ;
301
343
long ticks = reader . ReadInt64 ( ) ;
302
344
DateTimeOffset cutOff = new ( ticks , TimeSpan . Zero ) ;
303
- if ( clock . GetUtcNow ( ) >= cutOff )
345
+ if ( Clock . GetUtcNow ( ) >= cutOff )
304
346
{
305
347
// expired, delete, no exception for performance
306
348
logger . LogDebug ( "File cache expired {key}, {fileName}, deleting" , key , fileName ) ;
@@ -325,15 +367,15 @@ public FileCache(FileCacheOptions options,
325
367
throw new IOException ( "Byte counts are off for file cache item" ) ;
326
368
}
327
369
FileCacheItem < T > result ;
328
- if ( typeof ( T ) == byteArrayType )
370
+ if ( typeof ( T ) == byteArrayType )
329
371
{
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
+ }
332
374
else
333
375
{
334
376
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
+ }
337
379
logger . LogDebug ( "File cache hit {key}, {fileName}" , key , fileName ) ;
338
380
return result ;
339
381
}
@@ -403,7 +445,7 @@ public async Task SetAsync(string key, object value, CacheParameters cacheParame
403
445
{
404
446
using FileStream writerStream = new ( fileName , FileMode . Create , FileAccess . Write , FileShare . None ) ;
405
447
using BinaryWriter writer = new ( writerStream ) ;
406
- DateTimeOffset expires = clock . GetUtcNow ( ) + cacheParameters . Duration ;
448
+ DateTimeOffset expires = Clock . GetUtcNow ( ) + cacheParameters . Duration ;
407
449
writer . Write ( expires . Ticks ) ;
408
450
byte [ ] ? bytes = ( value is byte [ ] alreadyBytes ? alreadyBytes : Serializer . Serialize ( value ) ) ;
409
451
if ( bytes is not null )
@@ -516,4 +558,4 @@ public sealed class FileCacheOptions
516
558
/// Percentage (0 - 100) of free space remaining to trigger cleanup of files. Default is 15.
517
559
/// </summary>
518
560
public int FreeSpaceThreshold { get ; set ; } = 15 ;
519
- }
561
+ }
0 commit comments