22// The .NET Foundation licenses this file to you under the MIT license.
33
44using System . Diagnostics ;
5+ using System . Linq ;
56using Microsoft . Extensions . Caching . Memory ;
67
78namespace Microsoft . AspNetCore . OutputCaching . Memory ;
89
910internal sealed class MemoryOutputCacheStore : IOutputCacheStore
1011{
1112 private readonly MemoryCache _cache ;
12- private readonly Dictionary < string , HashSet < string > > _taggedEntries = new ( ) ;
13+ private readonly Dictionary < string , HashSet < TaggedEntry > > _taggedEntries = [ ] ;
1314 private readonly object _tagsLock = new ( ) ;
1415
1516 internal MemoryOutputCacheStore ( MemoryCache cache )
@@ -20,7 +21,7 @@ internal MemoryOutputCacheStore(MemoryCache cache)
2021 }
2122
2223 // For testing
23- internal Dictionary < string , HashSet < string > > TaggedEntries => _taggedEntries ;
24+ internal Dictionary < string , HashSet < string > > TaggedEntries => _taggedEntries . ToDictionary ( kvp => kvp . Key , kvp => kvp . Value . Select ( t => t . Key ) . ToHashSet ( ) ) ;
2425
2526 public ValueTask EvictByTagAsync ( string tag , CancellationToken cancellationToken )
2627 {
@@ -30,7 +31,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken
3031 {
3132 if ( _taggedEntries . TryGetValue ( tag , out var keys ) )
3233 {
33- if ( keys != null && keys . Count > 0 )
34+ if ( keys is { Count : > 0 } )
3435 {
3536 // If MemoryCache changed to run eviction callbacks inline in Remove, iterating over keys could throw
3637 // To prevent allocating a copy of the keys we check if the eviction callback ran,
@@ -40,7 +41,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken
4041 while ( i > 0 )
4142 {
4243 var oldCount = keys . Count ;
43- foreach ( var key in keys )
44+ foreach ( var ( key , _ ) in keys )
4445 {
4546 _cache . Remove ( key ) ;
4647 i -- ;
@@ -74,6 +75,8 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val
7475 ArgumentNullException . ThrowIfNull ( key ) ;
7576 ArgumentNullException . ThrowIfNull ( value ) ;
7677
78+ var entryId = Guid . NewGuid ( ) ;
79+
7780 if ( tags != null )
7881 {
7982 // Lock with SetEntry() to prevent EvictByTagAsync() from trying to remove a tag whose entry hasn't been added yet.
@@ -90,27 +93,27 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val
9093
9194 if ( ! _taggedEntries . TryGetValue ( tag , out var keys ) )
9295 {
93- keys = new HashSet < string > ( ) ;
96+ keys = new HashSet < TaggedEntry > ( ) ;
9497 _taggedEntries [ tag ] = keys ;
9598 }
9699
97100 Debug . Assert ( keys != null ) ;
98101
99- keys . Add ( key ) ;
102+ keys . Add ( new TaggedEntry ( key , entryId ) ) ;
100103 }
101104
102- SetEntry ( key , value , tags , validFor ) ;
105+ SetEntry ( key , value , tags , validFor , entryId ) ;
103106 }
104107 }
105108 else
106109 {
107- SetEntry ( key , value , tags , validFor ) ;
110+ SetEntry ( key , value , tags , validFor , entryId ) ;
108111 }
109112
110113 return ValueTask . CompletedTask ;
111114 }
112115
113- void SetEntry ( string key , byte [ ] value , string [ ] ? tags , TimeSpan validFor )
116+ private void SetEntry ( string key , byte [ ] value , string [ ] ? tags , TimeSpan validFor , Guid entryId )
114117 {
115118 Debug . Assert ( key != null ) ;
116119
@@ -120,30 +123,33 @@ void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor)
120123 Size = value . Length
121124 } ;
122125
123- if ( tags != null && tags . Length > 0 )
126+ if ( tags is { Length : > 0 } )
124127 {
125128 // Remove cache keys from tag lists when the entry is evicted
126- options . RegisterPostEvictionCallback ( RemoveFromTags , tags ) ;
129+ options . RegisterPostEvictionCallback ( RemoveFromTags , ( tags , entryId ) ) ;
127130 }
128131
129132 _cache . Set ( key , value , options ) ;
130133 }
131134
132- void RemoveFromTags ( object key , object ? value , EvictionReason reason , object ? state )
135+ private void RemoveFromTags ( object key , object ? value , EvictionReason reason , object ? state )
133136 {
134- var tags = state as string [ ] ;
137+ Debug . Assert ( state != null ) ;
138+
139+ var ( tags , entryId ) = ( ( string [ ] Tags , Guid EntryId ) ) state ;
135140
136141 Debug . Assert ( tags != null ) ;
137142 Debug . Assert ( tags . Length > 0 ) ;
138143 Debug . Assert ( key is string ) ;
144+ Debug . Assert ( entryId != Guid . Empty ) ;
139145
140146 lock ( _tagsLock )
141147 {
142148 foreach ( var tag in tags )
143149 {
144150 if ( _taggedEntries . TryGetValue ( tag , out var tagged ) )
145151 {
146- tagged . Remove ( ( string ) key ) ;
152+ tagged . Remove ( new TaggedEntry ( ( string ) key , entryId ) ) ;
147153
148154 // Remove the collection if there is no more keys in it
149155 if ( tagged . Count == 0 )
@@ -154,4 +160,6 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st
154160 }
155161 }
156162 }
163+
164+ private record TaggedEntry ( string Key , Guid EntryId ) ;
157165}
0 commit comments