1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
+ using System . Diagnostics ;
4
5
using Microsoft . Extensions . Caching . Memory ;
5
6
6
7
namespace Microsoft . AspNetCore . OutputCaching . Memory ;
7
8
8
9
internal sealed class MemoryOutputCacheStore : IOutputCacheStore
9
10
{
10
- private readonly IMemoryCache _cache ;
11
+ private readonly MemoryCache _cache ;
11
12
private readonly Dictionary < string , HashSet < string > > _taggedEntries = new ( ) ;
12
13
private readonly object _tagsLock = new ( ) ;
13
14
14
- internal MemoryOutputCacheStore ( IMemoryCache cache )
15
+ internal MemoryOutputCacheStore ( MemoryCache cache )
15
16
{
16
17
ArgumentNullException . ThrowIfNull ( cache ) ;
17
18
18
19
_cache = cache ;
19
20
}
20
21
22
+ // For testing
23
+ internal Dictionary < string , HashSet < string > > TaggedEntries => _taggedEntries ;
24
+
21
25
public ValueTask EvictByTagAsync ( string tag , CancellationToken cancellationToken )
22
26
{
23
27
ArgumentNullException . ThrowIfNull ( tag ) ;
@@ -26,12 +30,29 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken
26
30
{
27
31
if ( _taggedEntries . TryGetValue ( tag , out var keys ) )
28
32
{
29
- foreach ( var key in keys )
33
+ if ( keys != null && keys . Count > 0 )
30
34
{
31
- _cache . Remove ( key ) ;
32
- }
35
+ // If MemoryCache changed to run eviction callbacks inline in Remove, iterating over keys could throw
36
+ // To prevent allocating a copy of the keys we check if the eviction callback ran,
37
+ // and if it did we restart the loop.
33
38
34
- _taggedEntries . Remove ( tag ) ;
39
+ var i = keys . Count ;
40
+ while ( i > 0 )
41
+ {
42
+ var oldCount = keys . Count ;
43
+ foreach ( var key in keys )
44
+ {
45
+ _cache . Remove ( key ) ;
46
+ i -- ;
47
+ if ( oldCount != keys . Count )
48
+ {
49
+ // eviction callback ran inline, we need to restart the loop to avoid
50
+ // "collection modified while iterating" errors
51
+ break ;
52
+ }
53
+ }
54
+ }
55
+ }
35
56
}
36
57
}
37
58
@@ -62,35 +83,75 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val
62
83
{
63
84
foreach ( var tag in tags )
64
85
{
86
+ if ( tag is null )
87
+ {
88
+ throw new ArgumentException ( Resources . TagCannotBeNull ) ;
89
+ }
90
+
65
91
if ( ! _taggedEntries . TryGetValue ( tag , out var keys ) )
66
92
{
67
93
keys = new HashSet < string > ( ) ;
68
94
_taggedEntries [ tag ] = keys ;
69
95
}
70
96
97
+ Debug . Assert ( keys != null ) ;
98
+
71
99
keys . Add ( key ) ;
72
100
}
73
101
74
- SetEntry ( ) ;
102
+ SetEntry ( key , value , tags , validFor ) ;
75
103
}
76
104
}
77
105
else
78
106
{
79
- SetEntry ( ) ;
107
+ SetEntry ( key , value , tags , validFor ) ;
80
108
}
81
109
82
- void SetEntry ( )
110
+ return ValueTask . CompletedTask ;
111
+ }
112
+
113
+ void SetEntry ( string key , byte [ ] value , string [ ] ? tags , TimeSpan validFor )
114
+ {
115
+ Debug . Assert ( key != null ) ;
116
+
117
+ var options = new MemoryCacheEntryOptions
83
118
{
84
- _cache . Set (
85
- key ,
86
- value ,
87
- new MemoryCacheEntryOptions
88
- {
89
- AbsoluteExpirationRelativeToNow = validFor ,
90
- Size = value . Length
91
- } ) ;
119
+ AbsoluteExpirationRelativeToNow = validFor ,
120
+ Size = value . Length
121
+ } ;
122
+
123
+ if ( tags != null && tags . Length > 0 )
124
+ {
125
+ // Remove cache keys from tag lists when the entry is evicted
126
+ options . RegisterPostEvictionCallback ( RemoveFromTags , tags ) ;
92
127
}
93
128
94
- return ValueTask . CompletedTask ;
129
+ _cache . Set ( key , value , options ) ;
130
+ }
131
+
132
+ void RemoveFromTags ( object key , object ? value , EvictionReason reason , object ? state )
133
+ {
134
+ var tags = state as string [ ] ;
135
+
136
+ Debug . Assert ( tags != null ) ;
137
+ Debug . Assert ( tags . Length > 0 ) ;
138
+ Debug . Assert ( key is string ) ;
139
+
140
+ lock ( _tagsLock )
141
+ {
142
+ foreach ( var tag in tags )
143
+ {
144
+ if ( _taggedEntries . TryGetValue ( tag , out var tagged ) )
145
+ {
146
+ tagged . Remove ( ( string ) key ) ;
147
+
148
+ // Remove the collection if there is no more keys in it
149
+ if ( tagged . Count == 0 )
150
+ {
151
+ _taggedEntries . Remove ( tag ) ;
152
+ }
153
+ }
154
+ }
155
+ }
95
156
}
96
157
}
0 commit comments