Skip to content

M.E.C.M. Add TryGetValue(ReadOnlySpan<char>) API #112695

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

Merged
merged 7 commits into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -37,6 +37,13 @@ protected virtual void Dispose(bool disposing) { }
public Microsoft.Extensions.Caching.Memory.MemoryCacheStatistics? GetCurrentStatistics() { throw null; }
public void Remove(object key) { }
public bool TryGetValue(object key, out object? result) { throw null; }

#if NET9_0_OR_GREATER
[System.Runtime.CompilerServices.OverloadResolutionPriority(1)]
public bool TryGetValue(System.ReadOnlySpan<char> key, out object? value) { throw null; }
[System.Runtime.CompilerServices.OverloadResolutionPriority(1)]
public bool TryGetValue<TItem>(System.ReadOnlySpan<char> key, out TItem? value) { throw null; }
#endif
Copy link
Member

Choose a reason for hiding this comment

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

}
public partial class MemoryCacheOptions : Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Caching.Memory.MemoryCacheOptions>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,68 @@ public bool TryGetValue(object key, out object? result)
DateTime utcNow = UtcNow;

CoherentState coherentState = _coherentState; // Clear() can update the reference in the meantime
if (coherentState.TryGetValue(key, out CacheEntry? tmp))
coherentState.TryGetValue(key, out CacheEntry? entry); // note we rely on documented "default when fails" contract re the out
Copy link
Member

Choose a reason for hiding this comment

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

We could formalize the comment with an assert, e.g.

Suggested change
coherentState.TryGetValue(key, out CacheEntry? entry); // note we rely on documented "default when fails" contract re the out
bool success = coherentState.TryGetValue(key, out CacheEntry? entry);
Debug.Assert(success || entry is null, "We rely on documented 'default when fails' contract.");

return PostProcessTryGetValue(coherentState, utcNow, entry, out result);
}

#if NET9_0_OR_GREATER
/// <summary>
/// Gets the item associated with this key if present.
/// </summary>
/// <param name="key">A character span corresponding to a <see cref="string"/> identifying the requested entry.</param>
/// <param name="value">The located value or null.</param>
/// <returns>True if the key was found.</returns>
/// <remarks>This method allows values with <see cref="string"/> keys to be queried by content without allocating a new <see cref="string"/> instance.</remarks>
[OverloadResolutionPriority(1)]
Copy link
Member

Choose a reason for hiding this comment

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

@333fred, @jaredpar, just FYI on more use of ORPA.

public bool TryGetValue(ReadOnlySpan<char> key, out object? value)
{
CheckDisposed();

DateTime utcNow = UtcNow;

CoherentState coherentState = _coherentState; // Clear() can update the reference in the meantime
coherentState.TryGetValue(key, out CacheEntry? entry); // note we rely on documented "default when fails" contract re the out
return PostProcessTryGetValue(coherentState, utcNow, entry, out value);
}

/// <summary>
/// Gets the item associated with this key if present.
/// </summary>
/// <param name="key">A character span corresponding to a <see cref="string"/> identifying the requested entry.</param>
/// <param name="value">The located value or null.</param>
/// <returns>True if the key was found.</returns>
/// <remarks>This method allows values with <see cref="string"/> keys to be queried by content without allocating a new <see cref="string"/> instance.</remarks>
[OverloadResolutionPriority(1)]
public bool TryGetValue<TItem>(ReadOnlySpan<char> key, out TItem? value)
{
// this implementation intentionally based on (and consistent with) CacheExtensions.TryGetValue<TItem>
if (TryGetValue(key, out object? untyped))
{
if (untyped == null)
{
value = default;
return true;
}

if (untyped is TItem item)
{
value = item;
return true;
}
}

value = default;
return false;

}
#endif

private bool PostProcessTryGetValue(CoherentState coherentState, DateTime utcNow, CacheEntry? entry, out object? result)
{
// shared "get value" logic

if (entry is not null)
{
CacheEntry entry = tmp;
// Check if expired due to expiration tokens, timers, etc. and if so, remove it.
// Allow a stale Replaced value to be returned due to concurrent calls to SetExpired during SetEntry.
if (!entry.CheckExpired(utcNow) || entry.EvictionReason == EvictionReason.Replaced)
Expand Down Expand Up @@ -671,11 +730,27 @@ private sealed class CoherentState
{
private readonly ConcurrentDictionary<string, CacheEntry> _stringEntries = new ConcurrentDictionary<string, CacheEntry>(StringKeyComparer.Instance);
private readonly ConcurrentDictionary<object, CacheEntry> _nonStringEntries = new ConcurrentDictionary<object, CacheEntry>();

#if NET9_0_OR_GREATER
private readonly ConcurrentDictionary<string, CacheEntry>.AlternateLookup<ReadOnlySpan<char>> _stringAltLookup;

public CoherentState()
{
_stringAltLookup = _stringEntries.GetAlternateLookup<ReadOnlySpan<char>>();
}
#endif

internal long _cacheSize;

internal bool TryGetValue(object key, [NotNullWhen(true)] out CacheEntry? entry)
=> key is string s ? _stringEntries.TryGetValue(s, out entry) : _nonStringEntries.TryGetValue(key, out entry);

#if NET9_0_OR_GREATER
internal bool TryGetValue(ReadOnlySpan<char> key, [NotNullWhen(true)] out CacheEntry? entry)
=> _stringAltLookup.TryGetValue(key, out entry);
#endif


internal bool TryRemove(object key, [NotNullWhen(true)] out CacheEntry? entry)
=> key is string s ? _stringEntries.TryRemove(s, out entry) : _nonStringEntries.TryRemove(key, out entry);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using Xunit;

namespace Microsoft.Extensions.Caching.Memory.Tests
{
public class AltLookupTests
{
[Fact]
public void SimpleStringAccess()
{
// create a cache with unrelated other values
var cache = new MemoryCache(new MemoryCacheOptions());
cache.Set("unrelated key", new object());
cache.Set(Guid.NewGuid(), new object());

// our key, in various disguises
string stringKey = "my_key";
object objKey = stringKey;
ReadOnlySpan<char> stringSpanKey = stringKey.AsSpan();
// in the case of span, just to be super rigorous, we
// will also check an isolated span that isn't an interior pointer to a string
Span<char> isolated = stackalloc char[stringKey.Length];
stringKey.AsSpan().CopyTo(isolated);
ReadOnlySpan<char> isolatedSpanKey = isolated;

// not found initially (before added)
Assert.False(cache.TryGetValue(stringKey, out object? result));
Assert.Null(result);

Assert.False(cache.TryGetValue(objKey, out result));
Assert.Null(result);

#if NET9_0_OR_GREATER
Assert.False(cache.TryGetValue(stringSpanKey, out result));
Assert.Null(result);

Assert.False(cache.TryGetValue(isolatedSpanKey, out result));
Assert.Null(result);
#endif

// found after adding
object cachedValue = new();
cache.Set(stringKey, cachedValue);

Assert.True(cache.TryGetValue(stringKey, out result));
Assert.Same(cachedValue, result);

Assert.True(cache.TryGetValue(objKey, out result));
Assert.Same(cachedValue, result);

#if NET9_0_OR_GREATER
Assert.True(cache.TryGetValue(stringSpanKey, out result));
Assert.Same(cachedValue, result);

Assert.True(cache.TryGetValue(isolatedSpanKey, out result));
Assert.Same(cachedValue, result);
#endif

// not found after removing
cache.Remove(stringKey);

Assert.False(cache.TryGetValue(stringKey, out result));
Assert.Null(result);

Assert.False(cache.TryGetValue(objKey, out result));
Assert.Null(result);

#if NET9_0_OR_GREATER
Assert.False(cache.TryGetValue(stringSpanKey, out result));
Assert.Null(result);

Assert.False(cache.TryGetValue(isolatedSpanKey, out result));
Assert.Null(result);
#endif
}
}
}