Skip to content

Commit 7e9343b

Browse files
authored
MemoryCache: Add TryGetValue(ReadOnlySpan<char>) API (#112695)
* M.E.C.M. Add TryGetValue(ReadOnlySpan<char>) API fix #110504
1 parent 972b621 commit 7e9343b

File tree

3 files changed

+157
-4
lines changed

3 files changed

+157
-4
lines changed

src/libraries/Microsoft.Extensions.Caching.Memory/ref/Microsoft.Extensions.Caching.Memory.cs

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ protected virtual void Dispose(bool disposing) { }
3737
public Microsoft.Extensions.Caching.Memory.MemoryCacheStatistics? GetCurrentStatistics() { throw null; }
3838
public void Remove(object key) { }
3939
public bool TryGetValue(object key, out object? result) { throw null; }
40+
41+
#if NET9_0_OR_GREATER
42+
[System.Runtime.CompilerServices.OverloadResolutionPriority(1)]
43+
public bool TryGetValue(System.ReadOnlySpan<char> key, out object? value) { throw null; }
44+
[System.Runtime.CompilerServices.OverloadResolutionPriority(1)]
45+
public bool TryGetValue<TItem>(System.ReadOnlySpan<char> key, out TItem? value) { throw null; }
46+
#endif
4047
}
4148
public partial class MemoryCacheOptions : Microsoft.Extensions.Options.IOptions<Microsoft.Extensions.Caching.Memory.MemoryCacheOptions>
4249
{

src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs

+73-4
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,66 @@ public bool TryGetValue(object key, out object? result)
207207
ThrowHelper.ThrowIfNull(key);
208208

209209
CheckDisposed();
210+
CoherentState coherentState = _coherentState; // Clear() can update the reference in the meantime
211+
coherentState.TryGetValue(key, out CacheEntry? entry); // note we rely on documented "default when fails" contract re the out
212+
return PostProcessTryGetValue(coherentState, entry, out result);
213+
}
210214

211-
DateTime utcNow = UtcNow;
212-
215+
#if NET9_0_OR_GREATER
216+
/// <summary>
217+
/// Gets the item associated with this key if present.
218+
/// </summary>
219+
/// <param name="key">A character span corresponding to a <see cref="string"/> identifying the requested entry.</param>
220+
/// <param name="value">The located value or null.</param>
221+
/// <returns>True if the key was found.</returns>
222+
/// <remarks>This method allows values with <see cref="string"/> keys to be queried by content without allocating a new <see cref="string"/> instance.</remarks>
223+
[OverloadResolutionPriority(1)]
224+
public bool TryGetValue(ReadOnlySpan<char> key, out object? value)
225+
{
226+
CheckDisposed();
213227
CoherentState coherentState = _coherentState; // Clear() can update the reference in the meantime
214-
if (coherentState.TryGetValue(key, out CacheEntry? tmp))
228+
coherentState.TryGetValue(key, out CacheEntry? entry); // note we rely on documented "default when fails" contract re the out
229+
return PostProcessTryGetValue(coherentState, entry, out value);
230+
}
231+
232+
/// <summary>
233+
/// Gets the item associated with this key if present.
234+
/// </summary>
235+
/// <param name="key">A character span corresponding to a <see cref="string"/> identifying the requested entry.</param>
236+
/// <param name="value">The located value or null.</param>
237+
/// <returns>True if the key was found.</returns>
238+
/// <remarks>This method allows values with <see cref="string"/> keys to be queried by content without allocating a new <see cref="string"/> instance.</remarks>
239+
[OverloadResolutionPriority(1)]
240+
public bool TryGetValue<TItem>(ReadOnlySpan<char> key, out TItem? value)
241+
{
242+
// this implementation intentionally based on (and consistent with) CacheExtensions.TryGetValue<TItem>
243+
if (TryGetValue(key, out object? untyped))
244+
{
245+
if (untyped == null)
246+
{
247+
value = default;
248+
return true;
249+
}
250+
251+
if (untyped is TItem item)
252+
{
253+
value = item;
254+
return true;
255+
}
256+
}
257+
258+
value = default;
259+
return false;
260+
261+
}
262+
#endif
263+
264+
private bool PostProcessTryGetValue(CoherentState coherentState, CacheEntry? entry, out object? result)
265+
{
266+
// shared "get value" logic
267+
DateTime utcNow = UtcNow;
268+
if (entry is not null)
215269
{
216-
CacheEntry entry = tmp;
217270
// Check if expired due to expiration tokens, timers, etc. and if so, remove it.
218271
// Allow a stale Replaced value to be returned due to concurrent calls to SetExpired during SetEntry.
219272
if (!entry.CheckExpired(utcNow) || entry.EvictionReason == EvictionReason.Replaced)
@@ -671,11 +724,27 @@ private sealed class CoherentState
671724
{
672725
private readonly ConcurrentDictionary<string, CacheEntry> _stringEntries = new ConcurrentDictionary<string, CacheEntry>(StringKeyComparer.Instance);
673726
private readonly ConcurrentDictionary<object, CacheEntry> _nonStringEntries = new ConcurrentDictionary<object, CacheEntry>();
727+
728+
#if NET9_0_OR_GREATER
729+
private readonly ConcurrentDictionary<string, CacheEntry>.AlternateLookup<ReadOnlySpan<char>> _stringAltLookup;
730+
731+
public CoherentState()
732+
{
733+
_stringAltLookup = _stringEntries.GetAlternateLookup<ReadOnlySpan<char>>();
734+
}
735+
#endif
736+
674737
internal long _cacheSize;
675738

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

742+
#if NET9_0_OR_GREATER
743+
internal bool TryGetValue(ReadOnlySpan<char> key, [NotNullWhen(true)] out CacheEntry? entry)
744+
=> _stringAltLookup.TryGetValue(key, out entry);
745+
#endif
746+
747+
679748
internal bool TryRemove(object key, [NotNullWhen(true)] out CacheEntry? entry)
680749
=> key is string s ? _stringEntries.TryRemove(s, out entry) : _nonStringEntries.TryRemove(key, out entry);
681750

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System;
2+
using Xunit;
3+
4+
namespace Microsoft.Extensions.Caching.Memory.Tests
5+
{
6+
public class AltLookupTests
7+
{
8+
[Fact]
9+
public void SimpleStringAccess()
10+
{
11+
// create a cache with unrelated other values
12+
var cache = new MemoryCache(new MemoryCacheOptions());
13+
cache.Set("unrelated key", new object());
14+
cache.Set(Guid.NewGuid(), new object());
15+
16+
// our key, in various disguises
17+
string stringKey = "my_key";
18+
object objKey = stringKey;
19+
ReadOnlySpan<char> stringSpanKey = stringKey.AsSpan();
20+
// in the case of span, just to be super rigorous, we
21+
// will also check an isolated span that isn't an interior pointer to a string
22+
Span<char> isolated = stackalloc char[stringKey.Length];
23+
stringKey.AsSpan().CopyTo(isolated);
24+
ReadOnlySpan<char> isolatedSpanKey = isolated;
25+
26+
// not found initially (before added)
27+
Assert.False(cache.TryGetValue(stringKey, out object? result));
28+
Assert.Null(result);
29+
30+
Assert.False(cache.TryGetValue(objKey, out result));
31+
Assert.Null(result);
32+
33+
#if NET9_0_OR_GREATER
34+
Assert.False(cache.TryGetValue(stringSpanKey, out result));
35+
Assert.Null(result);
36+
37+
Assert.False(cache.TryGetValue(isolatedSpanKey, out result));
38+
Assert.Null(result);
39+
#endif
40+
41+
// found after adding
42+
object cachedValue = new();
43+
cache.Set(stringKey, cachedValue);
44+
45+
Assert.True(cache.TryGetValue(stringKey, out result));
46+
Assert.Same(cachedValue, result);
47+
48+
Assert.True(cache.TryGetValue(objKey, out result));
49+
Assert.Same(cachedValue, result);
50+
51+
#if NET9_0_OR_GREATER
52+
Assert.True(cache.TryGetValue(stringSpanKey, out result));
53+
Assert.Same(cachedValue, result);
54+
55+
Assert.True(cache.TryGetValue(isolatedSpanKey, out result));
56+
Assert.Same(cachedValue, result);
57+
#endif
58+
59+
// not found after removing
60+
cache.Remove(stringKey);
61+
62+
Assert.False(cache.TryGetValue(stringKey, out result));
63+
Assert.Null(result);
64+
65+
Assert.False(cache.TryGetValue(objKey, out result));
66+
Assert.Null(result);
67+
68+
#if NET9_0_OR_GREATER
69+
Assert.False(cache.TryGetValue(stringSpanKey, out result));
70+
Assert.Null(result);
71+
72+
Assert.False(cache.TryGetValue(isolatedSpanKey, out result));
73+
Assert.Null(result);
74+
#endif
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)