This repository has been archived by the owner on Nov 25, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 104
/
OutputCacheAttribute.cs
357 lines (302 loc) · 13.7 KB
/
OutputCacheAttribute.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.Caching;
using System.Security.Cryptography;
using System.Text;
using System.Web.Mvc.Properties;
using System.Web.UI;
namespace System.Web.Mvc
{
[SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed so that subclassed types can set properties in the default constructor.")]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class OutputCacheAttribute : ActionFilterAttribute, IExceptionFilter
{
private const string CacheKeyPrefix = "_MvcChildActionCache_";
private static ObjectCache _childActionCache;
private static object _childActionFilterFinishCallbackKey = new object();
private OutputCacheParameters _cacheSettings = new OutputCacheParameters { VaryByParam = "*" };
private Func<ObjectCache> _childActionCacheThunk = () => ChildActionCache;
private bool _locationWasSet;
private bool _noStoreWasSet;
public OutputCacheAttribute()
{
}
internal OutputCacheAttribute(ObjectCache childActionCache)
{
_childActionCacheThunk = () => childActionCache;
}
public string CacheProfile
{
get { return _cacheSettings.CacheProfile ?? String.Empty; }
set { _cacheSettings.CacheProfile = value; }
}
internal OutputCacheParameters CacheSettings
{
get { return _cacheSettings; }
}
public static ObjectCache ChildActionCache
{
get { return _childActionCache ?? MemoryCache.Default; }
set { _childActionCache = value; }
}
private ObjectCache ChildActionCacheInternal
{
get { return _childActionCacheThunk(); }
}
public int Duration
{
get { return _cacheSettings.Duration; }
set { _cacheSettings.Duration = value; }
}
public OutputCacheLocation Location
{
get { return _cacheSettings.Location; }
set
{
_cacheSettings.Location = value;
_locationWasSet = true;
}
}
public bool NoStore
{
get { return _cacheSettings.NoStore; }
set
{
_cacheSettings.NoStore = value;
_noStoreWasSet = true;
}
}
public string SqlDependency
{
get { return _cacheSettings.SqlDependency ?? String.Empty; }
set { _cacheSettings.SqlDependency = value; }
}
public string VaryByContentEncoding
{
get { return _cacheSettings.VaryByContentEncoding ?? String.Empty; }
set { _cacheSettings.VaryByContentEncoding = value; }
}
public string VaryByCustom
{
get { return _cacheSettings.VaryByCustom ?? String.Empty; }
set { _cacheSettings.VaryByCustom = value; }
}
public string VaryByHeader
{
get { return _cacheSettings.VaryByHeader ?? String.Empty; }
set { _cacheSettings.VaryByHeader = value; }
}
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Param", Justification = "Matches the @ OutputCache page directive. Suppressed in source because this is a special case suppression.")]
public string VaryByParam
{
get { return _cacheSettings.VaryByParam ?? String.Empty; }
set { _cacheSettings.VaryByParam = value; }
}
private static void ClearChildActionFilterFinishCallback(ControllerContext controllerContext)
{
controllerContext.HttpContext.Items.Remove(_childActionFilterFinishCallbackKey);
}
private static void CompleteChildAction(ControllerContext filterContext, bool wasException)
{
Action<bool> callback = GetChildActionFilterFinishCallback(filterContext);
if (callback != null)
{
ClearChildActionFilterFinishCallback(filterContext);
callback(wasException);
}
}
private static Action<bool> GetChildActionFilterFinishCallback(ControllerContext controllerContext)
{
return controllerContext.HttpContext.Items[_childActionFilterFinishCallbackKey] as Action<bool>;
}
internal string GetChildActionUniqueId(ActionExecutingContext filterContext)
{
StringBuilder uniqueIdBuilder = new StringBuilder();
// Start with a prefix, presuming that we share the cache with other users
uniqueIdBuilder.Append(CacheKeyPrefix);
// Unique ID of the action description
uniqueIdBuilder.Append(filterContext.ActionDescriptor.UniqueId);
// Unique ID from the VaryByCustom settings, if any
uniqueIdBuilder.Append(DescriptorUtil.CreateUniqueId(VaryByCustom));
if (!String.IsNullOrEmpty(VaryByCustom))
{
string varyByCustomResult = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, VaryByCustom);
uniqueIdBuilder.Append(varyByCustomResult);
}
// Unique ID from the VaryByParam settings, if any
uniqueIdBuilder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(VaryByParam)));
// The key is typically too long to be useful, so we use a cryptographic hash
// as the actual key (better randomization and key distribution, so small vary
// values will generate dramtically different keys).
using (SHA256 sha = SHA256.Create())
{
return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(uniqueIdBuilder.ToString())));
}
}
private static string GetUniqueIdFromActionParameters(ActionExecutingContext filterContext, IEnumerable<string> keys)
{
// Generate a unique ID of normalized key names + key values
var keyValues = new Dictionary<string, object>(filterContext.ActionParameters, StringComparer.OrdinalIgnoreCase);
keys = (keys ?? keyValues.Keys).Select(key => key.ToUpperInvariant())
.OrderBy(key => key, StringComparer.Ordinal);
return DescriptorUtil.CreateUniqueId(keys.Concat(keys.Select(key => keyValues.ContainsKey(key) ? keyValues[key] : null)));
}
public static bool IsChildActionCacheActive(ControllerContext controllerContext)
{
return GetChildActionFilterFinishCallback(controllerContext) != null;
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
// Complete the request if the child action threw an exception
if (filterContext.IsChildAction && filterContext.Exception != null)
{
CompleteChildAction(filterContext, wasException: true);
}
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (filterContext.IsChildAction)
{
ValidateChildActionConfiguration();
// Already actively being captured? (i.e., cached child action inside of cached child action)
// Realistically, this needs write substitution to do properly (including things like authentication)
if (GetChildActionFilterFinishCallback(filterContext) != null)
{
throw new InvalidOperationException(MvcResources.OutputCacheAttribute_CannotNestChildCache);
}
// Already cached?
string uniqueId = GetChildActionUniqueId(filterContext);
string cachedValue = ChildActionCacheInternal.Get(uniqueId) as string;
if (cachedValue != null)
{
filterContext.Result = new ContentResult() { Content = cachedValue };
return;
}
// Swap in a new TextWriter so we can capture the output
StringWriter cachingWriter = new StringWriter(CultureInfo.InvariantCulture);
TextWriter originalWriter = filterContext.HttpContext.Response.Output;
filterContext.HttpContext.Response.Output = cachingWriter;
// Set a finish callback to clean up
SetChildActionFilterFinishCallback(filterContext, wasException =>
{
// Restore original writer
filterContext.HttpContext.Response.Output = originalWriter;
// Grab output and write it
string capturedText = cachingWriter.ToString();
filterContext.HttpContext.Response.Write(capturedText);
// Only cache output if this wasn't an error
if (!wasException)
{
ChildActionCacheInternal.Add(uniqueId, capturedText, DateTimeOffset.UtcNow.AddSeconds(Duration));
}
});
}
}
public void OnException(ExceptionContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (filterContext.IsChildAction)
{
CompleteChildAction(filterContext, wasException: true);
}
}
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (!filterContext.IsChildAction)
{
// we need to call ProcessRequest() since there's no other way to set the Page.Response intrinsic
using (OutputCachedPage page = new OutputCachedPage(_cacheSettings))
{
page.ProcessRequest(HttpContext.Current);
}
}
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
if (filterContext == null)
{
throw new ArgumentNullException("filterContext");
}
if (filterContext.IsChildAction)
{
CompleteChildAction(filterContext, wasException: filterContext.Exception != null);
}
}
private static void SetChildActionFilterFinishCallback(ControllerContext controllerContext, Action<bool> callback)
{
controllerContext.HttpContext.Items[_childActionFilterFinishCallbackKey] = callback;
}
private static IEnumerable<string> SplitVaryByParam(string varyByParam)
{
if (String.Equals(varyByParam, "none", StringComparison.OrdinalIgnoreCase))
{
// Vary by nothing
return Enumerable.Empty<string>();
}
if (String.Equals(varyByParam, "*", StringComparison.OrdinalIgnoreCase))
{
// Vary by everything
return null;
}
return from part in varyByParam.Split(';') // Vary by specific parameters
let trimmed = part.Trim()
where !String.IsNullOrEmpty(trimmed)
select trimmed;
}
private void ValidateChildActionConfiguration()
{
if (Duration <= 0)
{
throw new InvalidOperationException(MvcResources.OutputCacheAttribute_InvalidDuration);
}
if (String.IsNullOrWhiteSpace(VaryByParam))
{
throw new InvalidOperationException(MvcResources.OutputCacheAttribute_InvalidVaryByParam);
}
if (!String.IsNullOrWhiteSpace(CacheProfile) ||
!String.IsNullOrWhiteSpace(SqlDependency) ||
!String.IsNullOrWhiteSpace(VaryByContentEncoding) ||
!String.IsNullOrWhiteSpace(VaryByHeader) ||
_locationWasSet || _noStoreWasSet)
{
throw new InvalidOperationException(MvcResources.OutputCacheAttribute_ChildAction_UnsupportedSetting);
}
}
[SuppressMessage("ASP.NET.Security", "CA5328:ValidateRequestShouldBeEnabled", Justification = "Instances of this type are not created in response to direct user input.")]
private sealed class OutputCachedPage : Page
{
private OutputCacheParameters _cacheSettings;
public OutputCachedPage(OutputCacheParameters cacheSettings)
{
// Tracing requires Page IDs to be unique.
ID = Guid.NewGuid().ToString();
_cacheSettings = cacheSettings;
}
protected override void FrameworkInitialize()
{
// when you put the <%@ OutputCache %> directive on a page, the generated code calls InitOutputCache() from here
base.FrameworkInitialize();
InitOutputCache(_cacheSettings);
}
}
}
}