Description
Background and motivation
This is follow-on work related to DefaultInterpolatedStringHandler and MemoryCache; the core idea here is that HybridCache
should be able to satisfy local "hit" requests synchronously without the code of a string key
parameter, instead leaving the key unmaterialized. This is achieved by taking the existing 2 HybridCache.GetOrCreateAsync
methods that take a string key
parameter, and add an overload that has ref DefaultInterpolatedStringHandler key
instead. Since Roslyn prefers this signature, existing HybridCache
code of the form:
var value = hybridCache.GetOrCreateAsync($"/some/{key}/path/{id}", ...);
will automatically update (at next recompile) to the new usage, with zero code changes required. We will also ensure documentation is clear about preferring this API.
Since this is a public abstraction with 1st and 3rd party implementations, and DefaultInterpolatedStringHandler
is a dangerous type, we do not blindly let implementations deal with this; the two new methods are non-virtual
, with a new virtual
method that takes the key as ReadOnlySpan<char> key
. That way, implementations are not exposed to the fiddly DefaultInterpolatedStringHandler
API and we do not risk leaks / double-pool-restore.
API compatibility:
This should probably target net10+ only, since it requires related features. If the MemoryCache
API gets backported to net9 (unlikely), in theory we could use the DefaultInterpolatedStringHandler
via unsafe-accessor, but: it seems preferable to consider this as net10+.
API Proposal
namespace Microsoft.Extensions.Caching.Hybrid;
public abstract partial class HybridCache
{
+ protected virtual ValueTask<T> GetOrCreateAsync<TState, T>(ReadOnlySpan<char> key, TState state, Func<TState, CancellationToken, ValueTask<T>> factory,
+ HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default)
+ => GetOrCreateAsync(key.ToString(), state, factory, options, tags, cancellationToken);
+ public ValueTask<T> GetOrCreateAsync<T>(ref DefaultInterpolatedStringHandler key, Func<CancellationToken, ValueTask<T>> factory,
+ HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ return GetOrCreateAsync(key.Text, factory, WrappedCallbackCache<T>.Instance, options, tags, cancellationToken);
+ }
+ finally
+ {
+ key.Clear();
+ }
+ }
+ public ValueTask<T> GetOrCreateAsync<TState, T>(ref DefaultInterpolatedStringHandler key, TState state, Func<TState, CancellationToken, ValueTask<T>> factory,
+ HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ return GetOrCreateAsync(key.Text, state, factory, options, tags, cancellationToken);
+ }
+ finally
+ {
+ key.Clear();
+ }
+ }
}
Notes:
- implementations shown here to illustrate that we're not breaking
abstract
rules - the
finally
withoutawait
does not in this instance represent premature cleanup; the span cannot traverse theasync
/await
boundary, so we know it is safe to callClear()
in all exit scenarios
The bottom two methods are the public
API surface, using try
/finally
to ensure correct handling of the key
. The first two method is the protected virtual
method that specific implementations may choose to override
to exploit allocation-free L1 lookups. If they do not choose to do so, we simply allocate the string
as we would have originally. This ensures the default behaviour is valid on existing implementations.
A note is added to clarify the slightly unusual usage of a span on a *Async
method:
<remarks>If the specific implementation cannot get the value synchronously, that implementation must copy the <paramref name="key"/> contents (using a leased buffer, string, or similar) before the asynchronous transition.</remarks>
This atypical usage is because of the high probability of local synchronous "hit" results, hence ValueTask<T>
as the return value.
API Usage
No change at the caller:
var value = hybridCache.GetOrCreateAsync($"/some/{key}/path/{id}", ...);
Activity