Skip to content

Commit 95cfe4b

Browse files
committed
implement stateful API
1 parent da66f08 commit 95cfe4b

File tree

4 files changed

+87
-16
lines changed

4 files changed

+87
-16
lines changed

src/Caching/Caching/src/DistributedCacheT.cs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,30 @@ public DistributedCache(IOptions<TypedDistributedCacheOptions> options, ICacheSe
2020
_ = options;
2121
}
2222

23-
public ValueTask<T> GetAsync(string key, Func<ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default)
23+
// for the simple usage scenario (no TState), pack the original callback as the "state", and use a wrapper function that just unrolls and invokes from the state
24+
static readonly Func<Func<CancellationToken, ValueTask<T>>, CancellationToken, ValueTask<T>> _wrapped = static (callback, ct) => callback(ct);
25+
public ValueTask<T> GetAsync(string key, Func<CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default)
26+
=> GetAsync(key, callback, _wrapped, options, cancellationToken);
27+
28+
public ValueTask<T> GetAsync<TState>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default)
2429
{
2530
ArgumentException.ThrowIfNullOrWhiteSpace(key);
2631
ArgumentNullException.ThrowIfNull(callback);
2732

2833
return _bufferBackend is not null
29-
? GetBufferedBackendAsync(key, callback, options, cancellationToken)
30-
: GetLegacyBackendAsync(key, callback, options, cancellationToken);
34+
? GetBufferedBackendAsync(key, state, callback, options, cancellationToken)
35+
: GetLegacyBackendAsync(key, state, callback, options, cancellationToken);
3136

3237
}
3338

34-
private ValueTask<T> GetBufferedBackendAsync(string key, Func<ValueTask<T>> callback, DistributedCacheEntryOptions? options, CancellationToken cancellationToken)
39+
private ValueTask<T> GetBufferedBackendAsync<TState>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options, CancellationToken cancellationToken)
3540
{
3641
var buffer = new RecyclableArrayBufferWriter<byte>();
3742
var pendingGet = _bufferBackend!.TryGetAsync(key, buffer, cancellationToken);
3843

3944
if (!pendingGet.IsCompletedSuccessfully)
4045
{
41-
return AwaitedBackend(this, key, callback, options, cancellationToken, buffer, pendingGet);
46+
return AwaitedBackend(this, key, state, callback, options, cancellationToken, buffer, pendingGet);
4247
}
4348

4449
// fast path; backend available immediately
@@ -50,9 +55,9 @@ private ValueTask<T> GetBufferedBackendAsync(string key, Func<ValueTask<T>> call
5055
}
5156

5257
// fall back to main code-path, but without the pending bytes (we've already checked those)
53-
return AwaitedBackend(this, key, callback, options, cancellationToken, buffer, default);
58+
return AwaitedBackend(this, key, state, callback, options, cancellationToken, buffer, default);
5459

55-
static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key, Func<ValueTask<T>> callback, DistributedCacheEntryOptions? options,
60+
static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options,
5661
CancellationToken cancellationToken, RecyclableArrayBufferWriter<byte> buffer, ValueTask<bool> pendingGet)
5762
{
5863
using (buffer)
@@ -62,7 +67,7 @@ static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key,
6267
return @this._serializer.Deserialize(new(buffer.GetCommittedMemory()));
6368
}
6469

65-
var value = await callback();
70+
var value = await callback(state, cancellationToken);
6671
if (value is null)
6772
{
6873
await @this._backend.RemoveAsync(key, cancellationToken);
@@ -79,12 +84,12 @@ static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key,
7984
}
8085
}
8186

82-
private ValueTask<T> GetLegacyBackendAsync(string key, Func<ValueTask<T>> callback, DistributedCacheEntryOptions? options, CancellationToken cancellationToken)
87+
private ValueTask<T> GetLegacyBackendAsync<TState>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options, CancellationToken cancellationToken)
8388
{
8489
var pendingBytes = _backend.GetAsync(key, cancellationToken);
8590
if (!pendingBytes.IsCompletedSuccessfully)
8691
{
87-
return AwaitedBackend(this, key, callback, options, cancellationToken, pendingBytes);
92+
return AwaitedBackend(this, key, state, callback, options, cancellationToken, pendingBytes);
8893
}
8994

9095
// fast path; backend available immediately
@@ -95,9 +100,9 @@ private ValueTask<T> GetLegacyBackendAsync(string key, Func<ValueTask<T>> callba
95100
}
96101

97102
// fall back to main code-path, but without the pending bytes (we've already checked those)
98-
return AwaitedBackend(this, key, callback, options, cancellationToken, null);
103+
return AwaitedBackend(this, key, state, callback, options, cancellationToken, null);
99104

100-
static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key, Func<ValueTask<T>> callback, DistributedCacheEntryOptions? options,
105+
static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options,
101106
CancellationToken cancellationToken, Task<byte[]?>? pendingBytes)
102107
{
103108
if (pendingBytes is not null)
@@ -109,7 +114,7 @@ static async ValueTask<T> AwaitedBackend(DistributedCache<T> @this, string key,
109114
}
110115
}
111116

112-
var value = await callback();
117+
var value = await callback(state, cancellationToken);
113118
if (value is null)
114119
{
115120
await @this._backend.RemoveAsync(key, cancellationToken);

src/Caching/Caching/src/IDistributedCacheT.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Diagnostics.CodeAnalysis;
5+
46
namespace Microsoft.Extensions.Caching.Distributed;
57

68
public interface IDistributedCache<T>
79
{
8-
ValueTask<T> GetAsync(string key, Func<ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default);
10+
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Does not cause ambiguity due to callback signature delta")]
11+
ValueTask<T> GetAsync(string key, Func<CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default);
12+
13+
[SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Does not cause ambiguity due to callback signature delta")]
14+
ValueTask<T> GetAsync<TState>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> callback, DistributedCacheEntryOptions? options = null, CancellationToken cancellationToken = default);
915

1016
ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default);
1117
}

src/Caching/Caching/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ Microsoft.Extensions.Caching.Distributed.ICacheSerializer<T>
44
Microsoft.Extensions.Caching.Distributed.ICacheSerializer<T>.Deserialize(System.Buffers.ReadOnlySequence<byte> source) -> T
55
Microsoft.Extensions.Caching.Distributed.ICacheSerializer<T>.Serialize(T value, System.Buffers.IBufferWriter<byte>! target) -> void
66
Microsoft.Extensions.Caching.Distributed.IDistributedCache<T>
7-
Microsoft.Extensions.Caching.Distributed.IDistributedCache<T>.GetAsync(string! key, System.Func<System.Threading.Tasks.ValueTask<T>>! callback, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<T>
7+
Microsoft.Extensions.Caching.Distributed.IDistributedCache<T>.GetAsync(string! key, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>>! callback, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<T>
8+
Microsoft.Extensions.Caching.Distributed.IDistributedCache<T>.GetAsync<TState>(string! key, TState state, System.Func<TState, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>>! callback, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<T>
89
Microsoft.Extensions.Caching.Distributed.IDistributedCache<T>.RemoveAsync(string! key, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask
910
Microsoft.Extensions.Caching.Distributed.TypedDistributedCacheOptions
1011
Microsoft.Extensions.Caching.Distributed.TypedDistributedCacheOptions.TypedDistributedCacheOptions() -> void

src/Caching/Caching/test/CacheConfigTests.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,57 @@ public async Task BasicUsage(bool useCustomBackend)
5151
}
5252
Assert.Equal(2, s.BackendCalls);
5353
}
54+
55+
[Theory]
56+
[InlineData(false)]
57+
[InlineData(true)]
58+
public async Task StatefulUsage(bool useCustomBackend)
59+
{
60+
var services = new ServiceCollection();
61+
62+
if (useCustomBackend)
63+
{
64+
services.AddSingleton<IDistributedCache, CustomBackend>();
65+
}
66+
services.AddTypedDistributedCache();
67+
services.AddScoped<SomeService>();
68+
var provider = services.BuildServiceProvider();
69+
70+
var s = provider.GetService<SomeService>();
71+
Assert.NotNull(s);
72+
73+
Assert.Equal(0, s.BackendCalls);
74+
var x = await s.GetFromCacheWithStateAsync(42);
75+
Assert.NotNull(x);
76+
Assert.Equal(42, x.Id);
77+
Assert.Equal(1, s.BackendCalls);
78+
79+
for (int i = 0; i < 10; i++)
80+
{
81+
var y = await s.GetFromCacheWithStateAsync(42);
82+
Assert.NotNull(y);
83+
Assert.NotSame(x, y);
84+
Assert.Equal(42, y.Id);
85+
}
86+
Assert.Equal(1, s.BackendCalls);
87+
88+
await Task.Delay(TimeSpan.FromSeconds(1.5)); // timeout
89+
90+
for (int i = 0; i < 10; i++)
91+
{
92+
var y = await s.GetFromCacheWithStateAsync(42);
93+
Assert.NotNull(y);
94+
Assert.Equal(42, y.Id);
95+
Assert.NotSame(x, y);
96+
}
97+
Assert.Equal(2, s.BackendCalls);
98+
99+
var z = await s.GetFromCacheWithStateAsync(43);
100+
Assert.NotNull(z);
101+
Assert.NotSame(x, z);
102+
Assert.Equal(43, z.Id);
103+
Assert.Equal(3, s.BackendCalls);
104+
}
54105
}
55106

56107
public class SomeService(IDistributedCache<Foo> cache)
@@ -62,8 +113,16 @@ private ValueTask<Foo> BackendAsync()
62113
var obj = new Foo { Id = Interlocked.Increment(ref _backendCalls) };
63114
return new(obj);
64115
}
116+
private ValueTask<Foo> BackendAsync(int id)
117+
{
118+
Interlocked.Increment(ref _backendCalls);
119+
var obj = new Foo { Id = id };
120+
return new(obj);
121+
}
122+
123+
public async Task<Foo> GetFromCacheAsync() => await cache.GetAsync("foos", _ => BackendAsync(), CacheExpiration);
65124

66-
public async Task<Foo> GetFromCacheAsync() => await cache.GetAsync("foos", () => BackendAsync(), CacheExpiration);
125+
public async Task<Foo> GetFromCacheWithStateAsync(int id) => await cache.GetAsync($"foos_{id}", (obj: this, id), static (state, ct) => state.obj.BackendAsync(state.id), CacheExpiration);
67126

68127
private static readonly DistributedCacheEntryOptions CacheExpiration = new DistributedCacheEntryOptions
69128
{

0 commit comments

Comments
 (0)