Skip to content

Commit a50dedf

Browse files
HybridCache: add net10 HybridCache zero-alloc fetch API (#114427)
* Add net10 HybridCache zero-alloc fetch API fixes #112866 * make ref API happy * copy/pasta * add clarifying comments * fix the namespace in the ref pack * Update src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCache.cs Co-authored-by: Theodore Tsirpanis <teo@tsirpanis.gr> * unify key handling --------- Co-authored-by: Theodore Tsirpanis <teo@tsirpanis.gr>
1 parent 2e77d32 commit a50dedf

File tree

4 files changed

+139
-2
lines changed

4 files changed

+139
-2
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public enum HybridCacheEntryFlags
193193
DisableUnderlyingData = 1 << 4,
194194
DisableCompression = 1 << 5,
195195
}
196-
public abstract class HybridCache
196+
public abstract partial class HybridCache
197197
{
198198
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,
199199
HybridCacheEntryOptions? options = null, System.Collections.Generic.IEnumerable<string>? tags = null, System.Threading.CancellationToken cancellationToken = default);

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum)</TargetFrameworks>
@@ -11,6 +11,9 @@
1111
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
1212
<Compile Include="Microsoft.Extensions.Caching.Abstractions.netcoreapp.cs" />
1313
</ItemGroup>
14+
<ItemGroup Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">
15+
<Compile Include="Microsoft.Extensions.Caching.Abstractions.net10.cs" />
16+
</ItemGroup>
1417

1518
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
1619
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
// Changes to this file must follow the https://aka.ms/api-review process.
5+
// ------------------------------------------------------------------------------
6+
7+
namespace Microsoft.Extensions.Caching.Hybrid
8+
{
9+
public abstract partial class HybridCache
10+
{
11+
public System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<T>(
12+
System.ReadOnlySpan<char> key,
13+
System.Func<System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
14+
Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions? options = null,
15+
System.Collections.Generic.IEnumerable<string>? tags = null,
16+
System.Threading.CancellationToken cancellationToken = default) => throw null;
17+
public virtual System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<TState, T>(
18+
System.ReadOnlySpan<char> key,
19+
TState state,
20+
System.Func<TState, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
21+
Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions? options = null,
22+
System.Collections.Generic.IEnumerable<string>? tags = null,
23+
System.Threading.CancellationToken cancellationToken = default) => throw null;
24+
public System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<T>(
25+
ref System.Runtime.CompilerServices.DefaultInterpolatedStringHandler key,
26+
System.Func<System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
27+
Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions? options = null,
28+
System.Collections.Generic.IEnumerable<string>? tags = null,
29+
System.Threading.CancellationToken cancellationToken = default) => throw null;
30+
public System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<TState, T>(
31+
ref System.Runtime.CompilerServices.DefaultInterpolatedStringHandler key,
32+
TState state,
33+
System.Func<TState, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
34+
Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions? options = null,
35+
System.Collections.Generic.IEnumerable<string>? tags = null,
36+
System.Threading.CancellationToken cancellationToken = default) => throw null;
37+
}
38+
}

src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCache.cs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections;
66
using System.Collections.Generic;
77
using System.Linq;
8+
using System.Runtime.CompilerServices;
89
using System.Threading;
910
using System.Threading.Tasks;
1011
using Microsoft.Extensions.Caching.Distributed;
@@ -45,6 +46,101 @@ public ValueTask<T> GetOrCreateAsync<T>(string key, Func<CancellationToken, Valu
4546
HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default)
4647
=> GetOrCreateAsync(key, factory, WrappedCallbackCache<T>.Instance, options, tags, cancellationToken);
4748

49+
#if NET10_0_OR_GREATER
50+
/// <summary>
51+
/// 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.
52+
/// </summary>
53+
/// <typeparam name="T">The type of the data being considered.</typeparam>
54+
/// <param name="key">The key of the entry to look for or create.</param>
55+
/// <param name="factory">Provides the underlying data service if the data is not available in the cache.</param>
56+
/// <param name="options">Additional options for this cache entry.</param>
57+
/// <param name="tags">The tags to associate with this cache item.</param>
58+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
59+
/// <returns>The data, either from cache or the underlying data service.</returns>
60+
public ValueTask<T> GetOrCreateAsync<T>(
61+
ReadOnlySpan<char> key,
62+
Func<CancellationToken, ValueTask<T>> factory,
63+
HybridCacheEntryOptions? options = null,
64+
IEnumerable<string>? tags = null,
65+
CancellationToken cancellationToken = default)
66+
=> GetOrCreateAsync(key, factory, WrappedCallbackCache<T>.Instance, options, tags, cancellationToken);
67+
68+
/// <summary>
69+
/// 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.
70+
/// </summary>
71+
/// <typeparam name="TState">The type of additional state required by <paramref name="factory"/>.</typeparam>
72+
/// <typeparam name="T">The type of the data being considered.</typeparam>
73+
/// <param name="key">The key of the entry to look for or create.</param>
74+
/// <param name="factory">Provides the underlying data service if the data is not available in the cache.</param>
75+
/// <param name="state">The state required for <paramref name="factory"/>.</param>
76+
/// <param name="options">Additional options for this cache entry.</param>
77+
/// <param name="tags">The tags to associate with this cache item.</param>
78+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
79+
/// <returns>The data, either from cache or the underlying data service.</returns>
80+
/// <remarks>Implementors may use the key span to attempt a local-cache synchronous 'get' without requiring the key as a <see cref="string"/>.</remarks>
81+
public virtual ValueTask<T> GetOrCreateAsync<TState, T>(
82+
ReadOnlySpan<char> key,
83+
TState state,
84+
Func<TState, CancellationToken, ValueTask<T>> factory,
85+
HybridCacheEntryOptions? options = null,
86+
IEnumerable<string>? tags = null,
87+
CancellationToken cancellationToken = default)
88+
=> GetOrCreateAsync(key.ToString(), state, factory, options, tags, cancellationToken);
89+
90+
/// <summary>
91+
/// 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.
92+
/// </summary>
93+
/// <typeparam name="T">The type of the data being considered.</typeparam>
94+
/// <param name="key">The key of the entry to look for or create.</param>
95+
/// <param name="factory">Provides the underlying data service if the data is not available in the cache.</param>
96+
/// <param name="options">Additional options for this cache entry.</param>
97+
/// <param name="tags">The tags to associate with this cache item.</param>
98+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
99+
/// <returns>The data, either from cache or the underlying data service.</returns>
100+
public ValueTask<T> GetOrCreateAsync<T>(
101+
ref DefaultInterpolatedStringHandler key,
102+
Func<CancellationToken, ValueTask<T>> factory,
103+
HybridCacheEntryOptions? options = null,
104+
IEnumerable<string>? tags = null,
105+
CancellationToken cancellationToken = default)
106+
{
107+
// It is *not* an error that this Clear occurs before the "await"; by definition, the implementation is *required* to copy
108+
// the value locally before an await, precisely because the ref-struct cannot bridge an await. Thus: we are fine to clean
109+
// the buffer even in the non-synchronous completion scenario.
110+
ValueTask<T> result = GetOrCreateAsync(key.Text, factory, WrappedCallbackCache<T>.Instance, options, tags, cancellationToken);
111+
key.Clear();
112+
return result;
113+
}
114+
115+
/// <summary>
116+
/// 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.
117+
/// </summary>
118+
/// <typeparam name="TState">The type of additional state required by <paramref name="factory"/>.</typeparam>
119+
/// <typeparam name="T">The type of the data being considered.</typeparam>
120+
/// <param name="key">The key of the entry to look for or create.</param>
121+
/// <param name="factory">Provides the underlying data service if the data is not available in the cache.</param>
122+
/// <param name="state">The state required for <paramref name="factory"/>.</param>
123+
/// <param name="options">Additional options for this cache entry.</param>
124+
/// <param name="tags">The tags to associate with this cache item.</param>
125+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
126+
/// <returns>The data, either from cache or the underlying data service.</returns>
127+
public ValueTask<T> GetOrCreateAsync<TState, T>(
128+
ref DefaultInterpolatedStringHandler key,
129+
TState state,
130+
Func<TState, CancellationToken, ValueTask<T>> factory,
131+
HybridCacheEntryOptions? options = null,
132+
IEnumerable<string>? tags = null,
133+
CancellationToken cancellationToken = default)
134+
{
135+
// It is *not* an error that this Clear occurs before the "await"; by definition, the implementation is *required* to copy
136+
// the value locally before an await, precisely because the ref-struct cannot bridge an await. Thus: we are fine to clean
137+
// the buffer even in the non-synchronous completion scenario.
138+
ValueTask<T> result = GetOrCreateAsync(key.Text, state, factory, options, tags, cancellationToken);
139+
key.Clear();
140+
return result;
141+
}
142+
#endif
143+
48144
private static class WrappedCallbackCache<T> // per-T memoized helper that allows GetOrCreateAsync<T> and GetOrCreateAsync<TState, T> to share an implementation
49145
{
50146
// 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

0 commit comments

Comments
 (0)