Skip to content
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

Caching: migrate HybridCache api surface from asp.net into runtime #103103

Merged
merged 13 commits into from
Jul 22, 2024
Merged
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 @@ -41,6 +41,13 @@ public partial interface IDistributedCache
void Set(string key, byte[] value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions options);
System.Threading.Tasks.Task SetAsync(string key, byte[] value, Microsoft.Extensions.Caching.Distributed.DistributedCacheEntryOptions options, System.Threading.CancellationToken token = default(System.Threading.CancellationToken));
}
public interface IBufferDistributedCache : IDistributedCache
{
bool TryGet(string key, System.Buffers.IBufferWriter<byte> destination);
System.Threading.Tasks.ValueTask<bool> TryGetAsync(string key, System.Buffers.IBufferWriter<byte> destination, System.Threading.CancellationToken token = default);
void Set(string key, System.Buffers.ReadOnlySequence<byte> value, DistributedCacheEntryOptions options);
System.Threading.Tasks.ValueTask SetAsync(string key, System.Buffers.ReadOnlySequence<byte> value, DistributedCacheEntryOptions options, System.Threading.CancellationToken token = default);
}
}
namespace Microsoft.Extensions.Caching.Memory
{
Expand Down Expand Up @@ -156,3 +163,55 @@ public SystemClock() { }
public System.DateTimeOffset UtcNow { get { throw null; } }
}
}
namespace Microsoft.Extensions.Caching.Hybrid
{
public partial interface IHybridCacheSerializer<T>
{
T Deserialize(System.Buffers.ReadOnlySequence<byte> source);
void Serialize(T value, System.Buffers.IBufferWriter<byte> target);
}
public interface IHybridCacheSerializerFactory
{
bool TryCreateSerializer<T>([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out IHybridCacheSerializer<T>? serializer);
}
public sealed class HybridCacheEntryOptions
{
public System.TimeSpan? Expiration { get; init; }
public System.TimeSpan? LocalCacheExpiration { get; init; }
public HybridCacheEntryFlags? Flags { get; init; }
}
[System.Flags]
public enum HybridCacheEntryFlags
{
None = 0,
DisableLocalCacheRead = 1 << 0,
DisableLocalCacheWrite = 1 << 1,
DisableLocalCache = DisableLocalCacheRead | DisableLocalCacheWrite,
DisableDistributedCacheRead = 1 << 2,
DisableDistributedCacheWrite = 1 << 3,
DisableDistributedCache = DisableDistributedCacheRead | DisableDistributedCacheWrite,
DisableUnderlyingData = 1 << 4,
DisableCompression = 1 << 5,
}
public abstract 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);

public System.Threading.Tasks.ValueTask<T> GetOrCreateAsync<T>(string key, System.Func<System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<T>> factory,
HybridCacheEntryOptions? options = null, System.Collections.Generic.IEnumerable<string>? tags = null, System.Threading.CancellationToken cancellationToken = default)
=> throw null;

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);

public abstract System.Threading.Tasks.ValueTask RemoveAsync(string key, System.Threading.CancellationToken cancellationToken = default);

public virtual System.Threading.Tasks.ValueTask RemoveAsync(System.Collections.Generic.IEnumerable<string> keys, System.Threading.CancellationToken cancellationToken = default)
=> throw null;

public virtual System.Threading.Tasks.ValueTask RemoveByTagAsync(System.Collections.Generic.IEnumerable<string> tags, System.Threading.CancellationToken cancellationToken = default)
stephentoub marked this conversation as resolved.
Show resolved Hide resolved
=> throw null;
public abstract System.Threading.Tasks.ValueTask RemoveByTagAsync(string tag, System.Threading.CancellationToken cancellationToken = default);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs"
Link="Common\System\Runtime\CompilerServices\IsExternalInit.cs" />

<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(SystemThreadingTasksExtensionsVersion)" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Provides multi-tier caching services building on <see cref="IDistributedCache"/> backends.
/// </summary>
public abstract class HybridCache
{
/// <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 is the data is not available in the cache.</param>
jozkee marked this conversation as resolved.
Show resolved Hide resolved
/// <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 abstract ValueTask<T> GetOrCreateAsync<TState, T>(string key, TState state, Func<TState, CancellationToken, ValueTask<T>> factory,
jozkee marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the order of TState / T be changed so that in all overloads the T is first?

HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default);

/// <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 is 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>(string 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);

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
public static readonly Func<Func<CancellationToken, ValueTask<T>>, CancellationToken, ValueTask<T>> Instance = static (callback, ct) => callback(ct);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually necessary? I seem to remember the C# compiler folding identical lambdas into the same method definition (but maybe I'm misremembering?)


/// <summary>
/// Asynchronously sets or overwrites the value associated with the key.
/// </summary>
/// <typeparam name="T">The type of the data being considered.</typeparam>
/// <param name="key">The key of the entry to create.</param>
/// <param name="value">The value to assign for this cache entry.</param>
/// <param name="options">Additional options for this cache entry.</param>
/// <param name="tags">The tags to associate with this cache entry.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
public abstract ValueTask SetAsync<T>(string key, T value, HybridCacheEntryOptions? options = null, IEnumerable<string>? tags = null, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously removes the value associated with the key if it exists.
/// </summary>
public abstract ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default);

/// <summary>
/// Asynchronously removes the value associated with the key if it exists.
/// </summary>
/// <remarks>Implementors should treat <c>null</c> as empty</remarks>
public virtual ValueTask RemoveAsync(IEnumerable<string> keys, CancellationToken cancellationToken = default)
{
return keys switch
{
// for consistency with GetOrCreate/Set: interpret null as "none"
null or ICollection<string> { Count: 0 } => default,
ICollection<string> { Count: 1 } => RemoveAsync(keys.First(), cancellationToken),
_ => ForEachAsync(this, keys, cancellationToken),
};

// default implementation is to call RemoveAsync for each key in turn
static async ValueTask ForEachAsync(HybridCache @this, IEnumerable<string> keys, CancellationToken cancellationToken)
{
foreach (var key in keys)
{
await @this.RemoveAsync(key, cancellationToken).ConfigureAwait(false);
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any tests to add in this repo for the functionality that's here?


/// <summary>
/// Asynchronously removes all values associated with the specified tags.
/// </summary>
/// <remarks>Implementors should treat <c>null</c> as empty</remarks>
public virtual ValueTask RemoveByTagAsync(IEnumerable<string> tags, CancellationToken cancellationToken = default)
jozkee marked this conversation as resolved.
Show resolved Hide resolved
{
return tags switch
{
// for consistency with GetOrCreate/Set: interpret null as "none"
null or ICollection<string> { Count: 0 } => default,
ICollection<string> { Count: 1 } => RemoveByTagAsync(tags.Single(), cancellationToken),
_ => ForEachAsync(this, tags, cancellationToken),
};

// default implementation is to call RemoveByTagAsync for each key in turn
static async ValueTask ForEachAsync(HybridCache @this, IEnumerable<string> keys, CancellationToken cancellationToken)
{
foreach (var key in keys)
{
await @this.RemoveByTagAsync(key, cancellationToken).ConfigureAwait(false);
}
}
}

/// <summary>
/// Asynchronously removes all values associated with the specified tag.
/// </summary>
public abstract ValueTask RemoveByTagAsync(string tag, CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Additional flags that apply to a <see cref="HybridCache"/> operation.
/// </summary>
[Flags]
public enum HybridCacheEntryFlags
{
/// <summary>
/// No additional flags.
/// </summary>
None = 0,
/// <summary>
/// Disables reading from the local in-process cache.
/// </summary>
DisableLocalCacheRead = 1 << 0,
/// <summary>
/// Disables writing to the local in-process cache.
/// </summary>
DisableLocalCacheWrite = 1 << 1,
/// <summary>
/// Disables both reading from and writing to the local in-process cache.
/// </summary>
DisableLocalCache = DisableLocalCacheRead | DisableLocalCacheWrite,
/// <summary>
/// Disables reading from the secondary distributed cache.
/// </summary>
DisableDistributedCacheRead = 1 << 2,
/// <summary>
/// Disables writing to the secondary distributed cache.
/// </summary>
DisableDistributedCacheWrite = 1 << 3,
/// <summary>
/// Disables both reading from and writing to the secondary distributed cache.
/// </summary>
DisableDistributedCache = DisableDistributedCacheRead | DisableDistributedCacheWrite,
/// <summary>
/// Only fetches the value from cache; does not attempt to access the underlying data store.
/// </summary>
DisableUnderlyingData = 1 << 4,
/// <summary>
/// Disables compression for this payload.
/// </summary>
DisableCompression = 1 << 5,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using Microsoft.Extensions.Caching.Distributed;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Additional options (expiration, etc.) that apply to a <see cref="HybridCache"/> operation. When options
/// can be specified at multiple levels (for example, globally and per-call), the values are composed; the
/// most granular non-null value is used, with null values being inherited. If no value is specified at
/// any level, the implementation may choose a reasonable default.
/// </summary>
public sealed class HybridCacheEntryOptions
{
/// <summary>
/// Gets or set the overall cache duration of this entry, passed to the backend distributed cache.
/// </summary>
public TimeSpan? Expiration { get; init; }

/// <remarks>
/// When retrieving a cached value from an external cache store, this value will be used to calculate the local
/// cache expiration, not exceeding the remaining overall cache lifetime.
/// </remarks>
public TimeSpan? LocalCacheExpiration { get; init; }

/// <summary>
/// Gets or sets additional flags that apply to the requested operation.
/// </summary>
public HybridCacheEntryFlags? Flags { get; init; }

// memoize when possible
private DistributedCacheEntryOptions? _dc;
internal DistributedCacheEntryOptions? ToDistributedCacheEntryOptions()
=> Expiration is null ? null : (_dc ??= new() { AbsoluteExpirationRelativeToNow = Expiration });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Per-type serialization/deserialization support for <see cref="HybridCache"/>.
/// </summary>
/// <typeparam name="T">The type being serialized/deserialized.</typeparam>
public interface IHybridCacheSerializer<T>
{
/// <summary>
/// Deserialize a <typeparamref name="T"/> value from the provided <paramref name="source"/>.
/// </summary>
T Deserialize(ReadOnlySequence<byte> source);

/// <summary>
/// Serialize <paramref name="value"/> to the provided <paramref name="target"/>.
/// </summary>
void Serialize(T value, IBufferWriter<byte> target);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Extensions.Caching.Hybrid;

/// <summary>
/// Factory provider for per-type <see cref="IHybridCacheSerializer{T}"/> instances.
/// </summary>
public interface IHybridCacheSerializerFactory
{
/// <summary>
/// Request a serializer for the provided type, if possible.
/// </summary>
/// <typeparam name="T">The type being serialized/deserialized.</typeparam>
/// <param name="serializer">The serializer.</param>
/// <returns><c>true</c> if the factory supports this type, <c>false</c> otherwise.</returns>
bool TryCreateSerializer<T>([NotNullWhen(true)] out IHybridCacheSerializer<T>? serializer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Extensions.Caching.Distributed; // intentional for parity with IDistributedCache

/// <summary>
/// Represents a distributed cache of serialized values, with support for low allocation data transfer.
/// </summary>
public interface IBufferDistributedCache : IDistributedCache
{
/// <summary>
/// Attempt to retrieve an existing cache item.
/// </summary>
/// <param name="key">The unique key for the cache item.</param>
/// <param name="destination">The target to write the cache contents on success.</param>
/// <returns><c>true</c> if the cache item is found, <c>false</c> otherwise.</returns>
/// <remarks>This is functionally similar to <see cref="IDistributedCache.Get(string)"/>, but avoids the array allocation.</remarks>
bool TryGet(string key, IBufferWriter<byte> destination);

/// <summary>
/// Asynchronously attempt to retrieve an existing cache entry.
/// </summary>
/// <param name="key">The unique key for the cache entry.</param>
/// <param name="destination">The target to write the cache contents on success.</param>
/// <param name="token">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns><c>true</c> if the cache entry is found, <c>false</c> otherwise.</returns>
/// <remarks>This is functionally similar to <see cref="IDistributedCache.GetAsync(string, CancellationToken)"/>, but avoids the array allocation.</remarks>
ValueTask<bool> TryGetAsync(string key, IBufferWriter<byte> destination, CancellationToken token = default);

/// <summary>
/// Sets or overwrites a cache item.
/// </summary>
/// <param name="key">The key of the entry to create.</param>
/// <param name="value">The value for this cache entry.</param>
/// <param name="options">The cache options for the entry.</param>
/// <remarks>This is functionally similar to <see cref="IDistributedCache.Set(string, byte[], DistributedCacheEntryOptions)"/>, but avoids the array allocation.</remarks>
void Set(string key, ReadOnlySequence<byte> value, DistributedCacheEntryOptions options);

/// <summary>
/// Asynchronously sets or overwrites a cache entry.
/// </summary>
/// <param name="key">The key of the entry to create.</param>
/// <param name="value">The value for this cache entry.</param>
/// <param name="options">The cache options for the value.</param>
/// <param name="token">The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <remarks>This is functionally similar to <see cref="IDistributedCache.SetAsync(string, byte[], DistributedCacheEntryOptions, CancellationToken)"/>, but avoids the array allocation.</remarks>
ValueTask SetAsync(string key, ReadOnlySequence<byte> value, DistributedCacheEntryOptions options, CancellationToken token = default);
}
Loading
Loading