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

[API Proposal]: Operations on partial IDistributedCache key values #76340

Open
Tracked by #77390
WhitWaldo opened this issue Sep 28, 2022 · 2 comments
Open
Tracked by #77390

[API Proposal]: Operations on partial IDistributedCache key values #76340

WhitWaldo opened this issue Sep 28, 2022 · 2 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-Caching
Milestone

Comments

@WhitWaldo
Copy link

Background and motivation

There are a great many reasons to cache values across disparate application types. Recently, I've been working to cache "recently used" values, such as those used in searches on a per-user basis.

Ideally, each value stored to the cache should be capable of independent expirations so I might save two values for a given user on Monday, with a 5 day expiry, three more on Wednesday and be confident that come Saturday, the values saved Monday will be expired and no longer available in the cache.

However, this scenario is frustrated by the current IDistributedCache API for a couple of reasons:

As it stands, I instead have to serialize each of my recent values along with their own local expiry values into a single list identified by a single known key, retrieve and deserialize them all for any search history queries and do a client-side evaluation of which values are still valid. This should ideally all be done within the IDistributedCache implementation to improve performance and eliminate surface area for bugs.

API Proposal

I would suggest implementing the following using the default interface methods introduced in C# 8.0. That said, I propose it as a discrete interface so as to simplify my example of it and more easily highlight what I'm articulating.

namespace Microsoft.Extensions.Caching.Distributed;

    public interface IExpressibleDistributedCache// : IDistributedCache //The name could use some additional contemplation
    {
        /// <summary>
        /// Gets the values for any key that matches the predicate.
        /// </summary>
        /// <param name="keyPredicate">An expression identifying the requested key(s).</param>
        /// <returns>The located value or null.</returns>
        IReadOnlyDictionary<string, byte[]> Get(Expression<Func<string, bool>> keyPredicate);

        /// <summary>
        /// Gets the values for any key that matches the predicate.
        /// </summary>
        /// <param name="keyPredicate">An expression identifying the requested key(s).</param>
        /// <param name="token">Optional. The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the located value or null.</returns>
        Task<IReadOnlyDictionary<string, byte[]>> GetAsync(Expression<Func<string, bool>> keyPredicate, CancellationToken token = default);

        /// <summary>
        /// Refreshes values in the cache based on their respective keys, resetting their sliding expiration timeout (if any).
        /// </summary>
        /// <param name="keyPredicate">A expression identifying the specified key(s).</param>
        void Refresh(Expression<Func<string, bool>> keyPredicate);

        /// <summary>
        /// Refreshes values in the cache based on their respective keys, resetting their sliding expiration timeout (if any).
        /// </summary>
        /// <param name="keyPredicate">An expression identifying the specified key(s).</param>
        /// <param name="token">Optional. The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
        Task RefreshAsync(Expression<Func<string, bool>> keyPredicate, CancellationToken token = default);

        /// <summary>
        /// Removes the value with the given key(s).
        /// </summary>
        /// <param name="keyPredicate">An expression identifying the requested value.</param>
        void Remove(Expression<Func<string, bool>> keyPredicate);

        /// <summary>
        /// Removes the value with the given key(s).
        /// </summary>
        /// <param name="keyPredicate">An expression identifying the specified key(s).</param>
        /// <param name="token">Optional. The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
        Task RemoveAsync(Expression<Func<string, bool>> keyPredicate, CancellationToken token = default);
    }

The reason to use Expression<Func<string, bool>> is so that the expression remains quotable for expression tree serializers such the recently open-sourced Bonsai (docs) to serialize the predicate across network boundaries as necessary.

API Usage

IExpressibleDistributedCache cache; //Injected

IExpressibleDistributedCache _cache;

//Get all the cached entries starting with "recentSearchTerms_user"
_cache.GetAsync(key => key.StartsWith("recentSearchTerms_user"));

//Refresh these cached entries
_cache.RefreshAsync(key => key.StartsWith("recentSearchTerms_user"));

//Remove the terms belonging to a specific user
_cache.RemoveAsync(key => key.StartsWith("recentSearchTerms_user123456_"));

Alternative Designs

Alternatively, provide a mechanism, per #36402 so clients can list all the keys (available at time of query) and perform the filtering locally before sending follow-up queries.

This would be an inferior solution though because it requires at least two round-trips and because #36402 is susceptible to keys no longer being available when the follow-up call is made. Rather, this approach would ensure that the operations were being run against as fresh a set of data can be made available and specifically how that's done (e.g. checkpoints, follower cache, etc.) is left as an implementation detail.

Risks

While it's reported that the Bonsai serializer has been used for several years now within Microsoft, it's only recently been open sourced, so it might be subject to bugs for more elaborate queries in the wild. This is why I kept the expression signature simple in my API example above.

If released as IExpressibleDistributedCache (a better name TBD), there's the risk that there are limited implementations of it due to the more commonly used and visible IDistributedCache. Thus my recommendation to instead release via default interface methods on IDistributedCache so that not only can existing implementations continue to work without issue, but a team-recommended approach for demonstrating the relatively new (at least publicly) Bonsai serialization can be provided out of the box.

@WhitWaldo WhitWaldo added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Sep 28, 2022
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Sep 28, 2022
@ghost
Copy link

ghost commented Sep 28, 2022

Tagging subscribers to this area: @dotnet/area-extensions-caching
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

There are a great many reasons to cache values across disparate application types. Recently, I've been working to cache "recently used" values, such as those used in searches on a per-user basis.

Ideally, each value stored to the cache should be capable of independent expirations so I might save two values for a given user on Monday, with a 5 day expiry, three more on Wednesday and be confident that come Saturday, the values saved Monday will be expired and no longer available in the cache.

However, this scenario is frustrated by the current IDistributedCache API for a couple of reasons:

As it stands, I instead have to serialize each of my recent values along with their own local expiry values into a single list identified by a single known key, retrieve and deserialize them all for any search history queries and do a client-side evaluation of which values are still valid. This should ideally all be done within the IDistributedCache implementation to improve performance and eliminate surface area for bugs.

API Proposal

I would suggest implementing the following using the default interface methods introduced in C# 8.0. That said, I propose it as a discrete interface so as to simplify my example of it and more easily highlight what I'm articulating.

namespace Microsoft.Extensions.Caching.Distributed;

    public interface IExpressibleDistributedCache// : IDistributedCache //The name could use some additional contemplation
    {
        /// <summary>
        /// Gets the values for any key that matches the predicate.
        /// </summary>
        /// <param name="keyPredicate">An expression identifying the requested key(s).</param>
        /// <returns>The located value or null.</returns>
        IReadOnlyDictionary<string, byte[]> Get(Expression<Func<string, bool>> keyPredicate);

        /// <summary>
        /// Gets the values for any key that matches the predicate.
        /// </summary>
        /// <param name="keyPredicate">An expression identifying the requested key(s).</param>
        /// <param name="token">Optional. The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the located value or null.</returns>
        Task<IReadOnlyDictionary<string, byte[]>> GetAsync(Expression<Func<string, bool>> keyPredicate, CancellationToken token = default);

        /// <summary>
        /// Refreshes values in the cache based on their respective keys, resetting their sliding expiration timeout (if any).
        /// </summary>
        /// <param name="keyPredicate">A expression identifying the specified key(s).</param>
        void Refresh(Expression<Func<string, bool>> keyPredicate);

        /// <summary>
        /// Refreshes values in the cache based on their respective keys, resetting their sliding expiration timeout (if any).
        /// </summary>
        /// <param name="keyPredicate">An expression identifying the specified key(s).</param>
        /// <param name="token">Optional. The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
        Task RefreshAsync(Expression<Func<string, bool>> keyPredicate, CancellationToken token = default);

        /// <summary>
        /// Removes the value with the given key(s).
        /// </summary>
        /// <param name="keyPredicate">An expression identifying the requested value.</param>
        void Remove(Expression<Func<string, bool>> keyPredicate);

        /// <summary>
        /// Removes the value with the given key(s).
        /// </summary>
        /// <param name="keyPredicate">An expression identifying the specified key(s).</param>
        /// <param name="token">Optional. The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
        /// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
        Task RemoveAsync(Expression<Func<string, bool>> keyPredicate, CancellationToken token = default);
    }

The reason to use Expression<Func<string, bool>> is so that the expression remains quotable for expression tree serializers such the recently open-sourced Bonsai (docs) to serialize the predicate across network boundaries as necessary.

API Usage

IExpressibleDistributedCache cache; //Injected

IExpressibleDistributedCache _cache;

//Get all the cached entries starting with "recentSearchTerms_user"
_cache.GetAsync(key => key.StartsWith("recentSearchTerms_user"));

//Refresh these cached entries
_cache.RefreshAsync(key => key.StartsWith("recentSearchTerms_user"));

//Remove the terms belonging to a specific user
_cache.RemoveAsync(key => key.StartsWith("recentSearchTerms_user123456_"));

Alternative Designs

Alternatively, provide a mechanism, per #36402 so clients can list all the keys (available at time of query) and perform the filtering locally before sending follow-up queries.

This would be an inferior solution though because it requires at least two round-trips and because #36402 is susceptible to keys no longer being available when the follow-up call is made. Rather, this approach would ensure that the operations were being run against as fresh a set of data can be made available and specifically how that's done (e.g. checkpoints, follower cache, etc.) is left as an implementation detail.

Risks

While it's reported that the Bonsai serializer has been used for several years now within Microsoft, it's only recently been open sourced, so it might be subject to bugs for more elaborate queries in the wild. This is why I kept the expression signature simple in my API example above.

If released as IExpressibleDistributedCache (a better name TBD), there's the risk that there are limited implementations of it due to the more commonly used and visible IDistributedCache. Thus my recommendation to instead release via default interface methods on IDistributedCache so that not only can existing implementations continue to work without issue, but a team-recommended approach for demonstrating the relatively new (at least publicly) Bonsai serialization can be provided out of the box.

Author: WhitWaldo
Assignees: -
Labels:

api-suggestion, untriaged, area-Extensions-Caching

Milestone: -

@tarekgh tarekgh added this to the 8.0.0 milestone Oct 5, 2022
@ghost ghost removed the untriaged New issue has not been triaged by the area owner label Oct 5, 2022
@adamsitnik adamsitnik modified the milestones: 8.0.0, Future Jul 27, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-Caching
Projects
None yet
Development

No branches or pull requests

4 participants