1
1
namespace Boxed . AspNetCore ;
2
2
3
3
using System ;
4
+ using System . Buffers ;
4
5
using System . Text . Json ;
5
6
using System . Threading ;
6
7
using System . Threading . Tasks ;
@@ -9,71 +10,200 @@ namespace Boxed.AspNetCore;
9
10
/// <summary>
10
11
/// <see cref="IDistributedCache"/> extension methods.
11
12
/// </summary>
13
+ /// <summary>
14
+ /// Provides a simple convenience wrapper around <see cref="IDistributedCache"/>; note that this implementation
15
+ /// does not attempt to avoid problems with multiple callers all invoking the "get" method at once when
16
+ /// data becomes evicted for cache ("stampeding"), or any other concerns such as returning stale data while
17
+ /// refresh occurs in the background - these are future considerations for the cache implementation.
18
+ /// </summary>
19
+ /// <remarks>The overloads taking <c>TState</c> are useful when used with <c>static</c> get methods, to avoid
20
+ /// "capture" overheads, but in most everyday scenarios, it may be more convenient to use the simpler stateless
21
+ /// version.</remarks>
12
22
public static class DistributedCacheExtensions
13
23
{
14
24
/// <summary>
15
- /// Gets the value of type <typeparamref name="T" /> with the specified key from the cache asynchronously by
16
- /// deserializing it from JSON format or returns <c>default(T)</c> if the key was not found.
25
+ /// Gets a value from cache, with a caller-supplied <paramref name="getMethod" /> (async, stateless) that is used if the value is not yet available.
17
26
/// </summary>
18
27
/// <typeparam name="T">The type of the value.</typeparam>
19
28
/// <param name="cache">The distributed cache.</param>
20
29
/// <param name="key">The cache item key.</param>
21
- /// <param name="jsonSerializerOptions">The JSON serializer options or <c>null</c> to use the default.</param>
22
- /// <param name="cancellationToken">The cancellation token.</param>
23
- /// <returns>The value of type <typeparamref name="T" /> or <c>null</c> if the key was not found.</returns>
24
- /// <exception cref="ArgumentNullException"><paramref name="cache"/> or <paramref name="key"/> is
25
- /// <c>null</c>.</exception>
26
- public static async Task < T ? > GetAsJsonAsync < T > (
30
+ /// <param name="getMethod">The method used to retrieve the item.</param>
31
+ /// <param name="options">The cache options.</param>
32
+ /// <param name="cancellation">The cancellation token.</param>
33
+ /// <returns>The cached value.</returns>
34
+ public static ValueTask < T > GetAsync < T > (
27
35
this IDistributedCache cache ,
28
36
string key ,
29
- JsonSerializerOptions ? jsonSerializerOptions = null ,
30
- CancellationToken cancellationToken = default )
31
- {
32
- ArgumentNullException . ThrowIfNull ( cache ) ;
33
- ArgumentNullException . ThrowIfNull ( key ) ;
37
+ Func < CancellationToken , ValueTask < T > > getMethod ,
38
+ DistributedCacheEntryOptions ? options = null ,
39
+ CancellationToken cancellation = default ) =>
40
+ GetAsyncSharedAsync < int , T > ( cache , key , state : 0 , getMethod , options , cancellation ) ; // use dummy state
34
41
35
- var bytes = await cache . GetAsync ( key , cancellationToken ) . ConfigureAwait ( false ) ;
36
- if ( bytes is null )
37
- {
38
- return default ;
39
- }
42
+ /// <summary>
43
+ /// Gets a value from cache, with a caller-supplied <paramref name="getMethod"/> (sync, stateless) that is used if the value is not yet available.
44
+ /// </summary>
45
+ /// <typeparam name="T">The type of the value.</typeparam>
46
+ /// <param name="cache">The distributed cache.</param>
47
+ /// <param name="key">The cache item key.</param>
48
+ /// <param name="getMethod">The method used to retrieve the item.</param>
49
+ /// <param name="options">The cache options.</param>
50
+ /// <param name="cancellation">The cancellation token.</param>
51
+ /// <returns>The cached value.</returns>
52
+ public static ValueTask < T > GetAsync < T > (
53
+ this IDistributedCache cache ,
54
+ string key ,
55
+ Func < T > getMethod ,
56
+ DistributedCacheEntryOptions ? options = null ,
57
+ CancellationToken cancellation = default ) =>
58
+ GetAsyncSharedAsync < int , T > ( cache , key , state : 0 , getMethod , options , cancellation ) ; // use dummy state
40
59
41
- return Deserialize < T > ( bytes , jsonSerializerOptions ) ;
42
- }
60
+ /// <summary>
61
+ /// Gets a value from cache, with a caller-supplied <paramref name="getMethod" /> (async, stateful) that is used if the value is not yet available.
62
+ /// </summary>
63
+ /// <typeparam name="TState">The type of the state.</typeparam>
64
+ /// <typeparam name="T">The type of the value.</typeparam>
65
+ /// <param name="cache">The distributed cache.</param>
66
+ /// <param name="key">The cache item key.</param>
67
+ /// <param name="state">The state.</param>
68
+ /// <param name="getMethod">The method used to retrieve the item.</param>
69
+ /// <param name="options">The cache options.</param>
70
+ /// <param name="cancellation">The cancellation token.</param>
71
+ /// <returns>
72
+ /// The cached value.
73
+ /// </returns>
74
+ public static ValueTask < T > GetAsync < TState , T > (
75
+ this IDistributedCache cache ,
76
+ string key ,
77
+ TState state ,
78
+ Func < TState , CancellationToken , ValueTask < T > > getMethod ,
79
+ DistributedCacheEntryOptions ? options = null ,
80
+ CancellationToken cancellation = default ) =>
81
+ GetAsyncSharedAsync < TState , T > ( cache , key , state , getMethod , options , cancellation ) ;
43
82
44
83
/// <summary>
45
- /// Sets the value of type <typeparamref name="T" /> with the specified key in the cache asynchronously by
46
- /// serializing it to JSON format.
84
+ /// Gets a value from cache, with a caller-supplied <paramref name="getMethod"/> (sync, stateful) that is used if the value is not yet available.
47
85
/// </summary>
86
+ /// <typeparam name="TState">The type of the state.</typeparam>
48
87
/// <typeparam name="T">The type of the value.</typeparam>
49
88
/// <param name="cache">The distributed cache.</param>
50
89
/// <param name="key">The cache item key.</param>
51
- /// <param name="value ">The value to cache .</param>
52
- /// <param name="options ">The cache options or <c>null</c> to use the default cache options .</param>
53
- /// <param name="jsonSerializerOptions ">The JSON serializer options or <c>null</c> to use the default .</param>
54
- /// <param name="cancellationToken ">The cancellation token.</param>
55
- /// <returns>The value of type <typeparamref name="T" /> or <c>null</c> if the key was not found.</returns>
56
- /// <exception cref="ArgumentNullException"><paramref name="cache"/> or <paramref name="key"/> is
57
- /// <c>null</c>.</exception >
58
- public static Task SetAsJsonAsync < T > (
90
+ /// <param name="state ">The state .</param>
91
+ /// <param name="getMethod ">The method used to retrieve the item .</param>
92
+ /// <param name="options ">The cache options.</param>
93
+ /// <param name="cancellation ">The cancellation token.</param>
94
+ /// <returns>
95
+ /// The cached value.
96
+ /// </returns >
97
+ public static ValueTask < T > GetAsync < TState , T > (
59
98
this IDistributedCache cache ,
60
99
string key ,
61
- T value ,
62
- DistributedCacheEntryOptions options ,
63
- JsonSerializerOptions ? jsonSerializerOptions = null ,
64
- CancellationToken cancellationToken = default )
65
- where T : class
100
+ TState state ,
101
+ Func < TState , T > getMethod ,
102
+ DistributedCacheEntryOptions ? options = null ,
103
+ CancellationToken cancellation = default ) =>
104
+ GetAsyncSharedAsync < TState , T > ( cache , key , state , getMethod , options , cancellation ) ;
105
+
106
+ /// <summary>
107
+ /// Provides a common implementation for the public-facing API, to avoid duplication.
108
+ /// </summary>
109
+ private static ValueTask < T > GetAsyncSharedAsync < TState , T > (
110
+ IDistributedCache cache ,
111
+ string key ,
112
+ TState state ,
113
+ Delegate getMethod ,
114
+ DistributedCacheEntryOptions ? options ,
115
+ CancellationToken cancellation )
66
116
{
67
117
ArgumentNullException . ThrowIfNull ( cache ) ;
68
- ArgumentNullException . ThrowIfNull ( key ) ;
69
118
70
- var bytes = JsonSerializer . SerializeToUtf8Bytes ( value , jsonSerializerOptions ) ;
71
- return cache . SetAsync ( key , bytes , options , cancellationToken ) ;
119
+ var pending = cache . GetAsync ( key , cancellation ) ;
120
+ if ( ! pending . IsCompletedSuccessfully )
121
+ {
122
+ // async-result was not available immediately; go full-async
123
+ return Awaited ( cache , key , pending , state , getMethod , options , cancellation ) ;
124
+ }
125
+
126
+ // GetAwaiter().GetResult() here is *not* "sync-over-async" - we've already
127
+ // validated that this data was available synchronously, and we're eliding
128
+ // the state machine overheads in the (hopefully high-hit-rate) success case
129
+ #pragma warning disable VSTHRD103 // Call async methods when in an async method
130
+ var bytes = pending . GetAwaiter ( ) . GetResult ( ) ;
131
+ #pragma warning restore VSTHRD103 // Call async methods when in an async method
132
+ if ( bytes is null )
133
+ {
134
+ // async-result was available but data is missing; go async for everything else
135
+ return Awaited ( cache , key , null , state , getMethod , options , cancellation ) ;
136
+ }
137
+
138
+ // data was available synchronously; deserialize
139
+ return new ( Deserialize < T > ( bytes ) ) ;
140
+
141
+ static async ValueTask < T > Awaited (
142
+ IDistributedCache cache , // the underlying cache
143
+ string key , // the key on the cache
144
+ Task < byte [ ] ? > ? pending , // incomplete "get bytes" operation, if any
145
+ TState state , // state possibly used by the get-method
146
+ Delegate getMethod , // the get-method supplied by the caller
147
+ DistributedCacheEntryOptions ? options , // cache expiration, etc
148
+ CancellationToken cancellation )
149
+ {
150
+ byte [ ] ? bytes ;
151
+ if ( pending is not null )
152
+ {
153
+ bytes = await pending . ConfigureAwait ( false ) ;
154
+ if ( bytes is not null )
155
+ {
156
+ // data was available asynchronously
157
+ return Deserialize < T > ( bytes ) ;
158
+ }
159
+ }
160
+
161
+ var result = getMethod switch
162
+ {
163
+ // we expect 4 use-cases; sync/async, with/without state
164
+ Func < TState , CancellationToken , ValueTask < T > > get => await get ( state , cancellation ) . ConfigureAwait ( false ) ,
165
+ Func < TState , T > get => get ( state ) ,
166
+ Func < CancellationToken , ValueTask < T > > get => await get ( cancellation ) . ConfigureAwait ( false ) ,
167
+ Func < T > get => get ( ) ,
168
+ _ => throw new ArgumentException ( "Unexpected get method found." , nameof ( getMethod ) ) ,
169
+ } ;
170
+ bytes = Serialize ( result ) ;
171
+
172
+ if ( options is null )
173
+ {
174
+ // not recommended; cache expiration should be considered
175
+ // important, usually
176
+ await cache . SetAsync ( key , bytes , cancellation ) . ConfigureAwait ( false ) ;
177
+ }
178
+ else
179
+ {
180
+ await cache . SetAsync ( key , bytes , options , cancellation ) . ConfigureAwait ( false ) ;
181
+ }
182
+
183
+ return result ;
184
+ }
72
185
}
73
186
74
- private static T ? Deserialize < T > ( byte [ ] bytes , JsonSerializerOptions ? jsonSerializerOptions )
187
+ // The current cache API is byte[]-based, but a wide range of
188
+ // serializer choices are possible; here we use the inbuilt
189
+ // System.Text.Json.JsonSerializer, which is a fair compromise
190
+ // between being easy to configure and use on general types,
191
+ // versus raw performance. Alternative (non-byte[]) storage
192
+ // mechanisms are under consideration.
193
+ //
194
+ // If it is likely that you will change serializers during
195
+ // upgrades (and you are using out-of-process storage), then
196
+ // you may wish to use a sentinel prefix before the payload,
197
+ // to allow you to safely switch between serializers;
198
+ // alternatively, you may choose to use a key-prefix so that
199
+ // the old data is simply not found (and expires naturally)
200
+ private static T Deserialize < T > ( byte [ ] bytes ) => JsonSerializer . Deserialize < T > ( bytes ) ! ;
201
+
202
+ private static byte [ ] Serialize < T > ( T value )
75
203
{
76
- var utf8JsonReader = new Utf8JsonReader ( bytes ) ;
77
- return JsonSerializer . Deserialize < T > ( ref utf8JsonReader , jsonSerializerOptions ) ;
204
+ var buffer = new ArrayBufferWriter < byte > ( ) ;
205
+ using var writer = new Utf8JsonWriter ( buffer ) ;
206
+ JsonSerializer . Serialize ( writer , value ) ;
207
+ return buffer . WrittenSpan . ToArray ( ) ;
78
208
}
79
209
}
0 commit comments