Skip to content

[API Proposal]: HybridCache: span-based fetch #112866

Open
@mgravell

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 without await does not in this instance represent premature cleanup; the span cannot traverse the async/await boundary, so we know it is safe to call Clear() 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementationarea-Extensions-CachingblockingMarks issues that we want to fast track in order to unblock other important workuntriagedNew issue has not been triaged by the area owner

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions