Skip to content

Commit ea9d53e

Browse files
mgravelljozkeestephentoub
authored
Caching: migrate HybridCache api surface from asp.net into runtime (#103103)
* HybridCache migration from aspnet * use cancellationToken instead of token * reapply dotnet/aspnetcore#56719 * Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCache.cs Co-authored-by: David Cantú <[email protected]> * Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCache.cs Co-authored-by: David Cantú <[email protected]> * Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCache.cs Co-authored-by: David Cantú <[email protected]> * prefer throw null * use IEnumerable<string> instead of IReadOnlyCollection<string> * remove suppressions * Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/IHybridCacheSerializer.cs Co-authored-by: Stephen Toub <[email protected]> * Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCacheEntryOptions.cs Co-authored-by: Stephen Toub <[email protected]> * Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCacheEntryOptions.cs Co-authored-by: Stephen Toub <[email protected]> * PR nits --------- Co-authored-by: David Cantú <[email protected]> Co-authored-by: Stephen Toub <[email protected]>
1 parent 4c21cb3 commit ea9d53e

9 files changed

+367
-2
lines changed

src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ public partial interface IDistributedCache
4141
void Set(string key, byte[] value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions options);
4242
System.Threading.Tasks.Task SetAsync(string key, byte[] value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken));
4343
}
44+
public interface IBufferDistributedCache : IDistributedCache
45+
{
46+
bool TryGet(string key, System.Buffers.IBufferWriter<byte> destination);
47+
System.Threading.Tasks.ValueTask<bool> TryGetAsync(string key, System.Buffers.IBufferWriter<byte> destination, System.Threading.CancellationToken token = default);
48+
void Set(string key, System.Buffers.ReadOnlySequence<byte> value, DistributedCacheEntryOptions options);
49+
System.Threading.Tasks.ValueTask SetAsync(string key, System.Buffers.ReadOnlySequence<byte> value, DistributedCacheEntryOptions options, System.Threading.CancellationToken token = default);
50+
}
4451
}
4552
namespace Microsoft.Extensions.Caching.Memory
4653
{
@@ -156,3 +163,55 @@ public SystemClock() { }
156163
public System.DateTimeOffset UtcNow { get { throw null; } }
157164
}
158165
}
166+
namespace Microsoft.Extensions.Caching.Hybrid
167+
{
168+
public partial interface IHybridCacheSerializer<T>
169+
{
170+
T Deserialize(System.Buffers.ReadOnlySequence<byte> source);
171+
void Serialize(T value, System.Buffers.IBufferWriter<byte> target);
172+
}
173+
public interface IHybridCacheSerializerFactory
174+
{
175+
bool TryCreateSerializer<T>([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out IHybridCacheSerializer<T>? serializer);
176+
}
177+
public sealed class HybridCacheEntryOptions
178+
{
179+
public System.TimeSpan? Expiration { get; init; }
180+
public System.TimeSpan? LocalCacheExpiration { get; init; }
181+
public HybridCacheEntryFlags? Flags { get; init; }
182+
}
183+
[System.Flags]
184+
public enum HybridCacheEntryFlags
185+
{
186+
None = 0,
187+
DisableLocalCacheRead = 1 << 0,
188+
DisableLocalCacheWrite = 1 << 1,
189+
DisableLocalCache = DisableLocalCacheRead | DisableLocalCacheWrite,
190+
DisableDistributedCacheRead = 1 << 2,
191+
DisableDistributedCacheWrite = 1 << 3,
192+
DisableDistributedCache = DisableDistributedCacheRead | DisableDistributedCacheWrite,
193+
DisableUnderlyingData = 1 << 4,
194+
DisableCompression = 1 << 5,
195+
}
196+
public abstract class HybridCache
197+
{
198+
public abstract System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<TState, T>(string key, TState state, System.Func<TState, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
199+
HybridCacheEntryOptions? options = null, System.Collections.Generic.IEnumerable<string>? tags = null, System.Threading.CancellationToken cancellationToken = default);
200+
201+
public System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<T>(string key, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
202+
HybridCacheEntryOptions? options = null, System.Collections.Generic.IEnumerable<string>? tags = null, System.Threading.CancellationToken cancellationToken = default)
203+
=> throw null;
204+
205+
public abstract System.Threading.Tasks.ValueTask SetAsync<T>(string key, T value, HybridCacheEntryOptions? options = null, System.Collections.Generic.IEnumerable<string>? tags = null, System.Threading.CancellationToken cancellationToken = default);
206+
207+
public abstract System.Threading.Tasks.ValueTask RemoveAsync(string key, System.Threading.CancellationToken cancellationToken = default);
208+
209+
public virtual System.Threading.Tasks.ValueTask RemoveAsync(System.Collections.Generic.IEnumerable<string> keys, System.Threading.CancellationToken cancellationToken = default)
210+
=> throw null;
211+
212+
public virtual System.Threading.Tasks.ValueTask RemoveByTagAsync(System.Collections.Generic.IEnumerable<string> tags, System.Threading.CancellationToken cancellationToken = default)
213+
=> throw null;
214+
public abstract System.Threading.Tasks.ValueTask RemoveByTagAsync(string tag, System.Threading.CancellationToken cancellationToken = default);
215+
}
216+
217+
}

src/libraries/Microsoft.Extensions.Caching.Abstractions/ref/Microsoft.Extensions.Caching.Abstractions.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
1717
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs"
1818
Link="Common\System\Runtime\CompilerServices\IsExternalInit.cs" />
19+
20+
<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(SystemThreadingTasksExtensionsVersion)" />
1921
</ItemGroup>
2022

2123
<ItemGroup>
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.Caching.Distributed;
11+
12+
namespace Microsoft.Extensions.Caching.Hybrid;
13+
14+
/// <summary>
15+
/// Provides multi-tier caching services building on <see cref="IDistributedCache"/> backends.
16+
/// </summary>
17+
public abstract class HybridCache
18+
{
19+
/// <summary>
20+
/// Asynchronously gets the value associated with the key if it exists, or generates a new entry using the provided key and a value from the given factory if the key is not found.
21+
/// </summary>
22+
/// <typeparam name="TState">The type of additional state required by <paramref name="factory"/>.</typeparam>
23+
/// <typeparam name="T">The type of the data being considered.</typeparam>
24+
/// <param name="key">The key of the entry to look for or create.</param>
25+
/// <param name="factory">Provides the underlying data service is the data is not available in the cache.</param>
26+
/// <param name="state">The state required for <paramref name="factory"/>.</param>
27+
/// <param name="options">Additional options for this cache entry.</param>
28+
/// <param name="tags">The tags to associate with this cache item.</param>
29+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
30+
/// <returns>The data, either from cache or the underlying data service.</returns>
31+
public abstract ValueTask<T> GetOrCreateAsync<TState, T>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> factory,
32+
HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default);
33+
34+
/// <summary>
35+
/// Asynchronously gets the value associated with the key if it exists, or generates a new entry using the provided key and a value from the given factory if the key is not found.
36+
/// </summary>
37+
/// <typeparam name="T">The type of the data being considered.</typeparam>
38+
/// <param name="key">The key of the entry to look for or create.</param>
39+
/// <param name="factory">Provides the underlying data service is the data is not available in the cache.</param>
40+
/// <param name="options">Additional options for this cache entry.</param>
41+
/// <param name="tags">The tags to associate with this cache item.</param>
42+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
43+
/// <returns>The data, either from cache or the underlying data service.</returns>
44+
public ValueTask<T> GetOrCreateAsync<T>(string key, Func<CancellationToken, ValueTask<T>> factory,
45+
HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default)
46+
=> GetOrCreateAsync(key, factory, WrappedCallbackCache<T>.Instance, options, tags, cancellationToken);
47+
48+
private static class WrappedCallbackCache<T> // per-T memoized helper that allows GetOrCreateAsync<T> and GetOrCreateAsync<TState, T> to share an implementation
49+
{
50+
// 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
51+
public static readonly Func<Func<CancellationToken, ValueTask<T>>, CancellationToken, ValueTask<T>> Instance = static (callback, ct) => callback(ct);
52+
}
53+
54+
/// <summary>
55+
/// Asynchronously sets or overwrites the value associated with the key.
56+
/// </summary>
57+
/// <typeparam name="T">The type of the data being considered.</typeparam>
58+
/// <param name="key">The key of the entry to create.</param>
59+
/// <param name="value">The value to assign for this cache entry.</param>
60+
/// <param name="options">Additional options for this cache entry.</param>
61+
/// <param name="tags">The tags to associate with this cache entry.</param>
62+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
63+
public abstract ValueTask SetAsync<T>(string key, T value, HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default);
64+
65+
/// <summary>
66+
/// Asynchronously removes the value associated with the key if it exists.
67+
/// </summary>
68+
public abstract ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default);
69+
70+
/// <summary>
71+
/// Asynchronously removes the value associated with the key if it exists.
72+
/// </summary>
73+
/// <remarks>Implementors should treat <c>null</c> as empty</remarks>
74+
public virtual ValueTask RemoveAsync(IEnumerable<string> keys, CancellationToken cancellationToken = default)
75+
{
76+
return keys switch
77+
{
78+
// for consistency with GetOrCreate/Set: interpret null as "none"
79+
null or ICollection<string> { Count: 0 } => default,
80+
ICollection<string> { Count: 1 } => RemoveAsync(keys.First(), cancellationToken),
81+
_ => ForEachAsync(this, keys, cancellationToken),
82+
};
83+
84+
// default implementation is to call RemoveAsync for each key in turn
85+
static async ValueTask ForEachAsync(HybridCache @this, IEnumerable<string> keys, CancellationToken cancellationToken)
86+
{
87+
foreach (var key in keys)
88+
{
89+
await @this.RemoveAsync(key, cancellationToken).ConfigureAwait(false);
90+
}
91+
}
92+
}
93+
94+
/// <summary>
95+
/// Asynchronously removes all values associated with the specified tags.
96+
/// </summary>
97+
/// <remarks>Implementors should treat <c>null</c> as empty</remarks>
98+
public virtual ValueTask RemoveByTagAsync(IEnumerable<string> tags, CancellationToken cancellationToken = default)
99+
{
100+
return tags switch
101+
{
102+
// for consistency with GetOrCreate/Set: interpret null as "none"
103+
null or ICollection<string> { Count: 0 } => default,
104+
ICollection<string> { Count: 1 } => RemoveByTagAsync(tags.Single(), cancellationToken),
105+
_ => ForEachAsync(this, tags, cancellationToken),
106+
};
107+
108+
// default implementation is to call RemoveByTagAsync for each key in turn
109+
static async ValueTask ForEachAsync(HybridCache @this, IEnumerable<string> keys, CancellationToken cancellationToken)
110+
{
111+
foreach (var key in keys)
112+
{
113+
await @this.RemoveByTagAsync(key, cancellationToken).ConfigureAwait(false);
114+
}
115+
}
116+
}
117+
118+
/// <summary>
119+
/// Asynchronously removes all values associated with the specified tag.
120+
/// </summary>
121+
public abstract ValueTask RemoveByTagAsync(string tag, CancellationToken cancellationToken = default);
122+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
6+
namespace Microsoft.Extensions.Caching.Hybrid;
7+
8+
/// <summary>
9+
/// Additional flags that apply to a <see cref="HybridCache"/> operation.
10+
/// </summary>
11+
[Flags]
12+
public enum HybridCacheEntryFlags
13+
{
14+
/// <summary>
15+
/// No additional flags.
16+
/// </summary>
17+
None = 0,
18+
/// <summary>
19+
/// Disables reading from the local in-process cache.
20+
/// </summary>
21+
DisableLocalCacheRead = 1 << 0,
22+
/// <summary>
23+
/// Disables writing to the local in-process cache.
24+
/// </summary>
25+
DisableLocalCacheWrite = 1 << 1,
26+
/// <summary>
27+
/// Disables both reading from and writing to the local in-process cache.
28+
/// </summary>
29+
DisableLocalCache = DisableLocalCacheRead | DisableLocalCacheWrite,
30+
/// <summary>
31+
/// Disables reading from the secondary distributed cache.
32+
/// </summary>
33+
DisableDistributedCacheRead = 1 << 2,
34+
/// <summary>
35+
/// Disables writing to the secondary distributed cache.
36+
/// </summary>
37+
DisableDistributedCacheWrite = 1 << 3,
38+
/// <summary>
39+
/// Disables both reading from and writing to the secondary distributed cache.
40+
/// </summary>
41+
DisableDistributedCache = DisableDistributedCacheRead | DisableDistributedCacheWrite,
42+
/// <summary>
43+
/// Only fetches the value from cache; does not attempt to access the underlying data store.
44+
/// </summary>
45+
DisableUnderlyingData = 1 << 4,
46+
/// <summary>
47+
/// Disables compression for this payload.
48+
/// </summary>
49+
DisableCompression = 1 << 5,
50+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using Microsoft.Extensions.Caching.Distributed;
6+
7+
namespace Microsoft.Extensions.Caching.Hybrid;
8+
9+
/// <summary>
10+
/// Additional options (expiration, etc.) that apply to a <see cref="HybridCache"/> operation. When options
11+
/// can be specified at multiple levels (for example, globally and per-call), the values are composed; the
12+
/// most granular non-null value is used, with null values being inherited. If no value is specified at
13+
/// any level, the implementation may choose a reasonable default.
14+
/// </summary>
15+
public sealed class HybridCacheEntryOptions
16+
{
17+
/// <summary>
18+
/// Gets or set the overall cache duration of this entry, passed to the backend distributed cache.
19+
/// </summary>
20+
public TimeSpan? Expiration { get; init; }
21+
22+
/// <remarks>
23+
/// When retrieving a cached value from an external cache store, this value will be used to calculate the local
24+
/// cache expiration, not exceeding the remaining overall cache lifetime.
25+
/// </remarks>
26+
public TimeSpan? LocalCacheExpiration { get; init; }
27+
28+
/// <summary>
29+
/// Gets or sets additional flags that apply to the requested operation.
30+
/// </summary>
31+
public HybridCacheEntryFlags? Flags { get; init; }
32+
33+
// memoize when possible
34+
private DistributedCacheEntryOptions? _dc;
35+
internal DistributedCacheEntryOptions? ToDistributedCacheEntryOptions()
36+
=> Expiration is null ? null : (_dc ??= new() { AbsoluteExpirationRelativeToNow = Expiration });
37+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Buffers;
5+
6+
namespace Microsoft.Extensions.Caching.Hybrid;
7+
8+
/// <summary>
9+
/// Per-type serialization/deserialization support for <see cref="HybridCache"/>.
10+
/// </summary>
11+
/// <typeparam name="T">The type being serialized/deserialized.</typeparam>
12+
public interface IHybridCacheSerializer<T>
13+
{
14+
/// <summary>
15+
/// Deserialize a <typeparamref name="T"/> value from the provided <paramref name="source"/>.
16+
/// </summary>
17+
T Deserialize(ReadOnlySequence<byte> source);
18+
19+
/// <summary>
20+
/// Serialize <paramref name="value"/> to the provided <paramref name="target"/>.
21+
/// </summary>
22+
void Serialize(T value, IBufferWriter<byte> target);
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
6+
namespace Microsoft.Extensions.Caching.Hybrid;
7+
8+
/// <summary>
9+
/// Factory provider for per-type <see cref="IHybridCacheSerializer{T}"/> instances.
10+
/// </summary>
11+
public interface IHybridCacheSerializerFactory
12+
{
13+
/// <summary>
14+
/// Request a serializer for the provided type, if possible.
15+
/// </summary>
16+
/// <typeparam name="T">The type being serialized/deserialized.</typeparam>
17+
/// <param name="serializer">The serializer.</param>
18+
/// <returns><c>true</c> if the factory supports this type, <c>false</c> otherwise.</returns>
19+
bool TryCreateSerializer<T>([NotNullWhen(true)] out IHybridCacheSerializer<T>? serializer);
20+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Buffers;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
8+
namespace Microsoft.Extensions.Caching.Distributed; // intentional for parity with IDistributedCache
9+
10+
/// <summary>
11+
/// Represents a distributed cache of serialized values, with support for low allocation data transfer.
12+
/// </summary>
13+
public interface IBufferDistributedCache : IDistributedCache
14+
{
15+
/// <summary>
16+
/// Attempt to retrieve an existing cache item.
17+
/// </summary>
18+
/// <param name="key">The unique key for the cache item.</param>
19+
/// <param name="destination">The target to write the cache contents on success.</param>
20+
/// <returns><c>true</c> if the cache item is found, <c>false</c> otherwise.</returns>
21+
/// <remarks>This is functionally similar to <see cref="IDistributedCache.Get(string)"/>, but avoids the array allocation.</remarks>
22+
bool TryGet(string key, IBufferWriter<byte> destination);
23+
24+
/// <summary>
25+
/// Asynchronously attempt to retrieve an existing cache entry.
26+
/// </summary>
27+
/// <param name="key">The unique key for the cache entry.</param>
28+
/// <param name="destination">The target to write the cache contents on success.</param>
29+
/// <param name="token">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
30+
/// <returns><c>true</c> if the cache entry is found, <c>false</c> otherwise.</returns>
31+
/// <remarks>This is functionally similar to <see cref="IDistributedCache.GetAsync(string, CancellationToken)"/>, but avoids the array allocation.</remarks>
32+
ValueTask<bool> TryGetAsync(string key, IBufferWriter<byte> destination, CancellationToken token = default);
33+
34+
/// <summary>
35+
/// Sets or overwrites a cache item.
36+
/// </summary>
37+
/// <param name="key">The key of the entry to create.</param>
38+
/// <param name="value">The value for this cache entry.</param>
39+
/// <param name="options">The cache options for the entry.</param>
40+
/// <remarks>This is functionally similar to <see cref="IDistributedCache.Set(string, byte[], DistributedCacheEntryOptions)"/>, but avoids the array allocation.</remarks>
41+
void Set(string key, ReadOnlySequence<byte> value, DistributedCacheEntryOptions options);
42+
43+
/// <summary>
44+
/// Asynchronously sets or overwrites a cache entry.
45+
/// </summary>
46+
/// <param name="key">The key of the entry to create.</param>
47+
/// <param name="value">The value for this cache entry.</param>
48+
/// <param name="options">The cache options for the value.</param>
49+
/// <param name="token">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
50+
/// <remarks>This is functionally similar to <see cref="IDistributedCache.SetAsync(string, byte[], DistributedCacheEntryOptions, CancellationToken)"/>, but avoids the array allocation.</remarks>
51+
ValueTask SetAsync(string key, ReadOnlySequence<byte> value, DistributedCacheEntryOptions options, CancellationToken token = default);
52+
}

0 commit comments

Comments
 (0)