• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ImmediatePlatform / Immediate.Cache / 29757980199

20 Jul 2026 04:04PM UTC coverage: 94.017% (-0.2%) from 94.218%
29757980199

Pull #134

github

web-flow
Merge 86e7e69f4 into 51eb6489f
Pull Request #134: Add support for nullable request/response

2 of 3 new or added lines in 1 file covered. (66.67%)

440 of 468 relevant lines covered (94.02%)

3.76 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.3
/src/Immediate.Cache.Shared/ApplicationCache.cs
1
using System.Diagnostics.CodeAnalysis;
2
using Immediate.Handlers.Shared;
3
using Microsoft.Extensions.Caching.Memory;
4

5
namespace Immediate.Cache.Shared;
6

7
/// <summary>
8
///                Base class for caching the results of an <see cref="IHandler{TRequest, TResponse}"/>.
9
/// </summary>
10
/// <typeparam name="TRequest">
11
///                The type of the handler request
12
/// </typeparam>
13
/// <typeparam name="TResponse">
14
///                The type of the handler response
15
/// </typeparam>
16
/// <param name="memoryCache">
17
///                An in-memory cache in which the result of a handler can be stored
18
/// </param>
19
/// <param name="handler">
20
///                The handler from which to cache data
21
/// </param>
22
public abstract class ApplicationCache<TRequest, TResponse>(
4✔
23
        IMemoryCache memoryCache,
4✔
24
        Owned<IHandler<TRequest, TResponse>> handler
4✔
25
)
4✔
26
        where TRequest : class?
27
        where TResponse : class?
28
{
29
        private readonly Lock _lock = new();
4✔
30

31
        /// <summary>
32
        ///            Transforms a <typeparamref name="TRequest"/> into a cache entry key.
33
        /// </summary>
34
        /// <param name="request">
35
        ///            The request being made to the handler.
36
        /// </param>
37
        /// <returns>
38
        ///            A <see langword="string" /> used as the key for storing the data in the cache.
39
        /// </returns>
40
        protected abstract string TransformKey(TRequest request);
41

42
        /// <summary>
43
        ///            Optionally set <see cref="MemoryCacheEntryOptions"/> for the cache entry
44
        /// </summary>
45
        /// <returns>
46
        ///            A <see cref="MemoryCacheEntryOptions"/> that contains configuration values for the cache entry.
47
        /// </returns>
48
        /// <remarks>
49
        ///            By default, this method sets the <see cref="MemoryCacheEntryOptions.SlidingExpiration"/> to a period of 5
50
        ///     minutes.
51
        /// </remarks>
52
        protected virtual MemoryCacheEntryOptions GetCacheEntryOptions() =>
53
                new()
4✔
54
                {
4✔
55
                        SlidingExpiration = TimeSpan.FromMinutes(5),
4✔
56
                };
4✔
57

58
        private CacheValue GetCacheValue(TRequest request)
59
        {
60
                var key = TransformKey(request);
4✔
61

62
                if (!memoryCache.TryGetValue(key, out var result))
4✔
63
                {
4✔
64
                        lock (_lock)
65
                        {
66
                                if (!memoryCache.TryGetValue(key, out result))
4✔
67
                                {
68
                                        using var entry = memoryCache.CreateEntry(key)
4✔
69
                                                .SetOptions(GetCacheEntryOptions());
4✔
70

71
                                        result = new CacheValue(request, handler);
4✔
72
                                        entry.Value = result;
4✔
73
                                }
74
                        }
×
75
                }
76

77
                if (result is not CacheValue cacheValue)
4✔
NEW
78
                        throw new InvalidOperationException($"An unknown type has been stored as the cache value for key `{key}`; Immediate.Cache is unable to operate.");
×
79

80
                return cacheValue;
4✔
81
        }
82

83
        /// <summary>
84
        ///            Retrieves a value from the cache, based on the <paramref name="request"/>. Executes the handler inside of a
85
        ///            temporary scope if the data is not currently available in the cache.
86
        /// </summary>
87
        /// <param name="request">
88
        ///                The request payload to be cached.
89
        /// </param>
90
        /// <param name="cancellationToken">
91
        ///                The token to monitor for cancellation requests. The default value is <see cref="CancellationToken.None"/>.
92
        /// </param>
93
        /// <returns>
94
        ///                The response payload from executing the handler.
95
        /// </returns>
96
        public ValueTask<TResponse> GetValue(TRequest request, CancellationToken cancellationToken = default) =>
97
                GetCacheValue(request).GetValue(cancellationToken);
4✔
98

99
        /// <summary>
100
        ///                Sets the value for a particular cache entry, bypassing the execution of the handler.
101
        /// </summary>
102
        /// <param name="request">
103
        ///                The request payload to be cached.
104
        /// </param>
105
        /// <param name="value">
106
        ///                The response payload to be cached.
107
        /// </param>
108
        public void SetValue(TRequest request, TResponse value) =>
109
                GetCacheValue(request).SetValue(value);
4✔
110

111
        /// <summary>
112
        ///            Removes the cached payload for a particular cache entry, forcing future requests for the same request to
113
        ///     execute the handler.
114
        /// </summary>
115
        /// <param name="request">
116
        ///                The request payload to be cached.
117
        /// </param>
118
        public void RemoveValue(TRequest request) =>
119
                GetCacheValue(request).RemoveValue();
4✔
120

121
        /// <summary>
122
        ///            Transforms the cached value, returning the newly transformed value.
123
        /// </summary>
124
        /// <param name="request">
125
        ///            The request payload to be cached.
126
        /// </param>
127
        /// <param name="transformer">
128
        ///            A method which will transformed the cached value into a new value.
129
        /// </param>
130
        /// <param name="token">
131
        ///            The <see cref="CancellationToken"/> to monitor for a cancellation request.
132
        /// </param>
133
        /// <returns>
134
        ///            The transformed value.
135
        /// </returns>
136
        /// <remarks>
137
        ///            The <paramref name="transformer"/> method may be called multiple times. <see cref="TransformValue(TRequest,
138
        ///     Func{TResponse, CancellationToken, ValueTask{TResponse}}, CancellationToken)"/> is implemented by retrieving
139
        ///     the value from cache, modifying it, and attempting to store the new value into the cache. Since the update
140
        ///     cannot be done inside of a critical section, the cached value may have changed between query and storage. If
141
        ///     this happens, the transformation process will be restarted.
142
        /// </remarks>
143
        protected ValueTask<TResponse> TransformValue(
144
                TRequest request,
145
                Func<TResponse, CancellationToken, ValueTask<TResponse>> transformer,
146
                CancellationToken token = default
147
        ) =>
148
                GetCacheValue(request).Transform(transformer, token);
4✔
149

150
        [SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "CancellationTokenSource does not need to be disposed here.")]
151
        private sealed class CacheValue(
4✔
152
                TRequest request,
4✔
153
                Owned<IHandler<TRequest, TResponse>> handler
4✔
154
        )
4✔
155
        {
156
                private CancellationTokenSource? _tokenSource;
157
                private TaskCompletionSource<TResponse>? _responseSource;
158
                private readonly Lock _lock = new();
4✔
159

160
                public async ValueTask<TResponse> GetValue(CancellationToken cancellationToken) =>
161
                        await GetHandlerTask().WaitAsync(cancellationToken).ConfigureAwait(false);
4✔
162

163
                private Task<TResponse> GetHandlerTask()
164
                {
4✔
165
                        lock (_lock)
166
                        {
167
                                if (_responseSource is { Task: { Status: not (TaskStatus.Faulted or TaskStatus.Canceled) } task })
4✔
168
                                        return task;
4✔
169

170
                                task = (_responseSource = new()).Task;
4✔
171
                                var cancellationTokenSource = _tokenSource = new();
4✔
172

173
                                // escape current sync context
174
                                _ = Task.Factory.StartNew(
4✔
175
                                        o => RunHandler((CancellationTokenSource)o!),
4✔
176
                                        cancellationTokenSource,
4✔
177
                                        CancellationToken.None,
4✔
178
                                        TaskCreationOptions.PreferFairness,
4✔
179
                                        TaskScheduler.Current
4✔
180
                                );
4✔
181

182
                                return task;
4✔
183
                        }
184
                }
4✔
185

186
                private async Task RunHandler(CancellationTokenSource tokenSource)
187
                {
4✔
188
                        lock (_lock)
189
                        {
190
                                if (_responseSource?.Task is { IsCompletedSuccessfully: true })
4✔
191
                                        return;
×
192
                        }
4✔
193

194
                        while (true)
195
                        {
196
                                try
197
                                {
198
                                        var token = tokenSource.Token;
4✔
199
                                        var scope = handler.GetScope(out var service);
4✔
200

201
                                        await using (scope.ConfigureAwait(false))
4✔
202
                                        {
203
                                                var response = await service
4✔
204
                                                        .HandleAsync(
4✔
205
                                                                request,
4✔
206
                                                                token
4✔
207
                                                        )
4✔
208
                                                        .ConfigureAwait(false);
4✔
209

210
                                                lock (_lock)
211
                                                {
212
                                                        if (!tokenSource.IsCancellationRequested)
4✔
213
                                                                _responseSource!.SetResult(response);
4✔
214
                                                }
4✔
215
                                        }
216
                                }
4✔
217
                                catch (OperationCanceledException) when (tokenSource.IsCancellationRequested)
4✔
218
                                {
219
                                }
4✔
220
#pragma warning disable CA1031 // Do not catch general exception types
221
                                // no one is listening to `RunHandler`; return the exception via `SetException`
222
                                catch (Exception ex)
4✔
223
#pragma warning restore CA1031
224
                                {
4✔
225
                                        lock (_lock)
226
                                        {
227
                                                if (!tokenSource.IsCancellationRequested)
4✔
228
                                                        _responseSource?.SetException(ex);
4✔
229
                                                return;
4✔
230
                                        }
231
                                }
232

233
                                lock (_lock)
234
                                {
235
                                        if (_responseSource is null or { Task.IsCompleted: true })
4✔
236
                                                return;
4✔
237

238
                                        if (_tokenSource is null or { IsCancellationRequested: true })
4✔
239
                                                _tokenSource = new();
4✔
240

241
                                        tokenSource = _tokenSource;
4✔
242
                                }
4✔
243
                        }
244
                }
4✔
245

246
                public void SetValue(TResponse response)
247
                {
4✔
248
                        lock (_lock)
249
                        {
250
                                if (_responseSource is null or { Task.IsCompleted: true })
4✔
251
                                        _responseSource = new();
4✔
252

253
                                _responseSource.SetResult(response);
4✔
254

255
                                _tokenSource?.Cancel();
4✔
256
                                _tokenSource = null;
4✔
257
                        }
4✔
258
                }
4✔
259

260
                [SuppressMessage("Performance", "CA1849:Call async methods when in an async method", Justification = "Inside a `lock`, and testing `IsCompleted` first.")]
261
                public async ValueTask<TResponse> Transform(
262
                        Func<TResponse, CancellationToken, ValueTask<TResponse>> transformer,
263
                        CancellationToken token
264
                )
265
                {
266
                        while (true)
4✔
267
                        {
268
                                if (await Core(transformer, token).ConfigureAwait(false) is (true, var response))
4✔
269
                                        return response;
4✔
270

271
                                _ = await GetHandlerTask().WaitAsync(token).ConfigureAwait(false);
4✔
272
                        }
273

274
                        async ValueTask<(bool, TResponse)> Core(
275
                                Func<TResponse, CancellationToken, ValueTask<TResponse>> transformer,
276
                                CancellationToken token
277
                        )
278
                        {
279
                                if (GetTask() is not { } task)
4✔
280
                                        return default;
4✔
281

282
                                var response = await task.ConfigureAwait(false);
4✔
283
                                var result = await transformer(response, token).ConfigureAwait(false);
4✔
284

285
                                lock (_lock)
286
                                {
287
                                        if (!ReferenceEquals(_responseSource?.Task, task))
4✔
288
                                                return default;
4✔
289

290
                                        (_responseSource = new()).SetResult(result);
4✔
291
                                        return (true, result);
4✔
292
                                }
293
                        }
4✔
294

295
                        [SuppressMessage(
296
                                "Design",
297
                                "MA0022:Return Task.FromResult instead of returning null",
298
                                Justification = "`null` is actually desired here"
299
                        )]
300
                        Task<TResponse>? GetTask()
301
                        {
4✔
302
                                lock (_lock)
303
                                {
304
                                        if (_responseSource is { Task: { IsCompleted: true } task })
4✔
305
                                                return task;
4✔
306
                                }
4✔
307

308
                                return null;
4✔
309
                        }
4✔
310
                }
3✔
311

312
                public void RemoveValue()
313
                {
4✔
314
                        lock (_lock)
315
                        {
316
                                if (_responseSource is { Task.IsCompleted: true })
4✔
317
                                        _responseSource = null;
4✔
318

319
                                _tokenSource?.Cancel();
4✔
320
                                _tokenSource = null;
4✔
321
                        }
4✔
322
                }
4✔
323
        }
324
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc