Skip to content

HybridCache: add net10 HybridCache zero-alloc fetch API #114427

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ public enum HybridCacheEntryFlags
DisableUnderlyingData = 1 << 4,
DisableCompression = 1 << 5,
}
public abstract class HybridCache
public abstract partial class HybridCache
{
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,
HybridCacheEntryOptions? options = null, System.Collections.Generic.IEnumerable<string>? tags = null, System.Threading.CancellationToken cancellationToken = default);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// ------------------------------------------------------------------------------
// Changes to this file must follow the https://aka.ms/api-review process.
// ------------------------------------------------------------------------------

namespace Microsoft.Extensions.Caching.Hybrid
{
public abstract partial class HybridCache
{
public System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<T>(
System.ReadOnlySpan<char> key,
System.Func<System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions? options = null,
System.Collections.Generic.IEnumerable<string>? tags = null,
System.Threading.CancellationToken cancellationToken = default) => throw null;
public virtual System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<TState, T>(
System.ReadOnlySpan<char> key,
TState state,
System.Func<TState, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions? options = null,
System.Collections.Generic.IEnumerable<string>? tags = null,
System.Threading.CancellationToken cancellationToken = default) => throw null;
public System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<T>(
ref System.Runtime.CompilerServices.DefaultInterpolatedStringHandler key,
System.Func<System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions? options = null,
System.Collections.Generic.IEnumerable<string>? tags = null,
System.Threading.CancellationToken cancellationToken = default) => throw null;
public System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<TState, T>(
ref System.Runtime.CompilerServices.DefaultInterpolatedStringHandler key,
TState state,
System.Func<TState, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions? options = null,
System.Collections.Generic.IEnumerable<string>? tags = null,
System.Threading.CancellationToken cancellationToken = default) => throw null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
Expand Down Expand Up @@ -45,6 +46,101 @@ public ValueTask<T> GetOrCreateAsync<T>(string key, Func<CancellationToken, Valu
HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default)
=> GetOrCreateAsync(key, factory, WrappedCallbackCache<T>.Instance, options, tags, cancellationToken);

#if NET10_0_OR_GREATER
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The type of the data being considered.</typeparam>
/// <param name="key">The key of the entry to look for or create.</param>
/// <param name="factory">Provides the underlying data service if the data is not available in the cache.</param>
/// <param name="options">Additional options for this cache entry.</param>
/// <param name="tags">The tags to associate with this cache item.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The data, either from cache or the underlying data service.</returns>
public ValueTask<T> GetOrCreateAsync<T>(
ReadOnlySpan<char> key,
Func<CancellationToken, ValueTask<T>> factory,
HybridCacheEntryOptions? options = null,
IEnumerable<string>? tags = null,
CancellationToken cancellationToken = default)
=> GetOrCreateAsync(key, factory, WrappedCallbackCache<T>.Instance, options, tags, cancellationToken);

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TState">The type of additional state required by <paramref name="factory"/>.</typeparam>
/// <typeparam name="T">The type of the data being considered.</typeparam>
/// <param name="key">The key of the entry to look for or create.</param>
/// <param name="factory">Provides the underlying data service if the data is not available in the cache.</param>
/// <param name="state">The state required for <paramref name="factory"/>.</param>
/// <param name="options">Additional options for this cache entry.</param>
/// <param name="tags">The tags to associate with this cache item.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The data, either from cache or the underlying data service.</returns>
/// <remarks>Implementors may use the key span to attempt a local-cache synchronous 'get' without requiring the key as a <see cref="string"/>.</remarks>
public 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);

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The type of the data being considered.</typeparam>
/// <param name="key">The key of the entry to look for or create.</param>
/// <param name="factory">Provides the underlying data service if the data is not available in the cache.</param>
/// <param name="options">Additional options for this cache entry.</param>
/// <param name="tags">The tags to associate with this cache item.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The data, either from cache or the underlying data service.</returns>
public ValueTask<T> GetOrCreateAsync<T>(
ref DefaultInterpolatedStringHandler key,
Func<CancellationToken, ValueTask<T>> factory,
HybridCacheEntryOptions? options = null,
IEnumerable<string>? tags = null,
CancellationToken cancellationToken = default)
{
// It is *not* an error that this Clear occurs before the "await"; by definition, the implementation is *required* to copy
// the value locally before an await, precisely because the ref-struct cannot bridge an await. Thus: we are fine to clean
// the buffer even in the non-synchronous completion scenario.
ValueTask<T> result = GetOrCreateAsync(key.Text, factory, WrappedCallbackCache<T>.Instance, options, tags, cancellationToken);
key.Clear();
return result;
}

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TState">The type of additional state required by <paramref name="factory"/>.</typeparam>
/// <typeparam name="T">The type of the data being considered.</typeparam>
/// <param name="key">The key of the entry to look for or create.</param>
/// <param name="factory">Provides the underlying data service if the data is not available in the cache.</param>
/// <param name="state">The state required for <paramref name="factory"/>.</param>
/// <param name="options">Additional options for this cache entry.</param>
/// <param name="tags">The tags to associate with this cache item.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The data, either from cache or the underlying data service.</returns>
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)
{
// It is *not* an error that this Clear occurs before the "await"; by definition, the implementation is *required* to copy
// the value locally before an await, precisely because the ref-struct cannot bridge an await. Thus: we are fine to clean
// the buffer even in the non-synchronous completion scenario.
ValueTask<T> result = GetOrCreateAsync(key.Text, state, factory, options, tags, cancellationToken);
key.Clear();
return result;
}
#endif

private static class WrappedCallbackCache<T> // per-T memoized helper that allows GetOrCreateAsync<T> and GetOrCreateAsync<TState, T> to share an implementation
{
// 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
Expand Down
Loading