PageRenderTime 55ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/mcs/class/System.Web.Mvc3/Mvc/OutputCacheAttribute.cs

http://github.com/mono/mono
C# | 337 lines | 262 code | 55 blank | 20 comment | 33 complexity | 80675adee9cf643af9a600d535e5e60a MD5 | raw file
Possible License(s): GPL-2.0, CC-BY-SA-3.0, LGPL-2.0, MPL-2.0-no-copyleft-exception, LGPL-2.1, Unlicense, Apache-2.0
  1. namespace System.Web.Mvc {
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Globalization;
  6. using System.IO;
  7. using System.Linq;
  8. using System.Runtime.Caching;
  9. using System.Security.Cryptography;
  10. using System.Text;
  11. using System.Web;
  12. using System.Web.Mvc.Resources;
  13. using System.Web.UI;
  14. [SuppressMessage("Microsoft.Performance", "CA1813:AvoidUnsealedAttributes", Justification = "Unsealed so that subclassed types can set properties in the default constructor.")]
  15. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
  16. public class OutputCacheAttribute : ActionFilterAttribute, IExceptionFilter {
  17. private OutputCacheParameters _cacheSettings = new OutputCacheParameters { VaryByParam = "*" };
  18. private const string _cacheKeyPrefix = "_MvcChildActionCache_";
  19. private static ObjectCache _childActionCache;
  20. private Func<ObjectCache> _childActionCacheThunk = () => ChildActionCache;
  21. private static object _childActionFilterFinishCallbackKey = new object();
  22. private bool _locationWasSet;
  23. private bool _noStoreWasSet;
  24. public OutputCacheAttribute() {
  25. }
  26. internal OutputCacheAttribute(ObjectCache childActionCache) {
  27. _childActionCacheThunk = () => childActionCache;
  28. }
  29. public string CacheProfile {
  30. get {
  31. return _cacheSettings.CacheProfile ?? String.Empty;
  32. }
  33. set {
  34. _cacheSettings.CacheProfile = value;
  35. }
  36. }
  37. internal OutputCacheParameters CacheSettings {
  38. get {
  39. return _cacheSettings;
  40. }
  41. }
  42. public static ObjectCache ChildActionCache {
  43. get {
  44. return _childActionCache ?? MemoryCache.Default;
  45. }
  46. set {
  47. _childActionCache = value;
  48. }
  49. }
  50. private ObjectCache ChildActionCacheInternal {
  51. get {
  52. return _childActionCacheThunk();
  53. }
  54. }
  55. public int Duration {
  56. get {
  57. return _cacheSettings.Duration;
  58. }
  59. set {
  60. _cacheSettings.Duration = value;
  61. }
  62. }
  63. public OutputCacheLocation Location {
  64. get {
  65. return _cacheSettings.Location;
  66. }
  67. set {
  68. _cacheSettings.Location = value;
  69. _locationWasSet = true;
  70. }
  71. }
  72. public bool NoStore {
  73. get {
  74. return _cacheSettings.NoStore;
  75. }
  76. set {
  77. _cacheSettings.NoStore = value;
  78. _noStoreWasSet = true;
  79. }
  80. }
  81. public string SqlDependency {
  82. get {
  83. return _cacheSettings.SqlDependency ?? String.Empty;
  84. }
  85. set {
  86. _cacheSettings.SqlDependency = value;
  87. }
  88. }
  89. public string VaryByContentEncoding {
  90. get {
  91. return _cacheSettings.VaryByContentEncoding ?? String.Empty;
  92. }
  93. set {
  94. _cacheSettings.VaryByContentEncoding = value;
  95. }
  96. }
  97. public string VaryByCustom {
  98. get {
  99. return _cacheSettings.VaryByCustom ?? String.Empty;
  100. }
  101. set {
  102. _cacheSettings.VaryByCustom = value;
  103. }
  104. }
  105. public string VaryByHeader {
  106. get {
  107. return _cacheSettings.VaryByHeader ?? String.Empty;
  108. }
  109. set {
  110. _cacheSettings.VaryByHeader = value;
  111. }
  112. }
  113. [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Param", Justification = "Matches the @ OutputCache page directive.")]
  114. public string VaryByParam {
  115. get {
  116. return _cacheSettings.VaryByParam ?? String.Empty;
  117. }
  118. set {
  119. _cacheSettings.VaryByParam = value;
  120. }
  121. }
  122. private static void ClearChildActionFilterFinishCallback(ControllerContext controllerContext) {
  123. controllerContext.HttpContext.Items.Remove(_childActionFilterFinishCallbackKey);
  124. }
  125. private static void CompleteChildAction(ControllerContext filterContext, bool wasException) {
  126. Action<bool> callback = GetChildActionFilterFinishCallback(filterContext);
  127. if (callback != null) {
  128. ClearChildActionFilterFinishCallback(filterContext);
  129. callback(wasException);
  130. }
  131. }
  132. private static Action<bool> GetChildActionFilterFinishCallback(ControllerContext controllerContext) {
  133. return controllerContext.HttpContext.Items[_childActionFilterFinishCallbackKey] as Action<bool>;
  134. }
  135. internal string GetChildActionUniqueId(ActionExecutingContext filterContext) {
  136. StringBuilder uniqueIdBuilder = new StringBuilder();
  137. // Start with a prefix, presuming that we share the cache with other users
  138. uniqueIdBuilder.Append(_cacheKeyPrefix);
  139. // Unique ID of the action description
  140. uniqueIdBuilder.Append(filterContext.ActionDescriptor.UniqueId);
  141. // Unique ID from the VaryByCustom settings, if any
  142. uniqueIdBuilder.Append(DescriptorUtil.CreateUniqueId(VaryByCustom));
  143. if (!String.IsNullOrEmpty(VaryByCustom)) {
  144. string varyByCustomResult = filterContext.HttpContext.ApplicationInstance.GetVaryByCustomString(HttpContext.Current, VaryByCustom);
  145. uniqueIdBuilder.Append(varyByCustomResult);
  146. }
  147. // Unique ID from the VaryByParam settings, if any
  148. uniqueIdBuilder.Append(GetUniqueIdFromActionParameters(filterContext, SplitVaryByParam(VaryByParam)));
  149. // The key is typically too long to be useful, so we use a cryptographic hash
  150. // as the actual key (better randomization and key distribution, so small vary
  151. // values will generate dramtically different keys).
  152. using (SHA256 sha = SHA256.Create()) {
  153. return Convert.ToBase64String(sha.ComputeHash(Encoding.UTF8.GetBytes(uniqueIdBuilder.ToString())));
  154. }
  155. }
  156. private static string GetUniqueIdFromActionParameters(ActionExecutingContext filterContext, IEnumerable<string> keys) {
  157. // Generate a unique ID of normalized key names + key values
  158. var keyValues = new Dictionary<string, object>(filterContext.ActionParameters, StringComparer.OrdinalIgnoreCase);
  159. keys = (keys ?? keyValues.Keys).Select(key => key.ToUpperInvariant())
  160. .OrderBy(key => key, StringComparer.Ordinal);
  161. return DescriptorUtil.CreateUniqueId(keys.Concat(keys.Select(key => keyValues.ContainsKey(key) ? keyValues[key] : null)));
  162. }
  163. public static bool IsChildActionCacheActive(ControllerContext controllerContext) {
  164. return GetChildActionFilterFinishCallback(controllerContext) != null;
  165. }
  166. public override void OnActionExecuted(ActionExecutedContext filterContext) {
  167. if (filterContext == null) {
  168. throw new ArgumentNullException("filterContext");
  169. }
  170. // Complete the request if the child action threw an exception
  171. if (filterContext.IsChildAction && filterContext.Exception != null) {
  172. CompleteChildAction(filterContext, wasException: true);
  173. }
  174. }
  175. public override void OnActionExecuting(ActionExecutingContext filterContext) {
  176. if (filterContext == null) {
  177. throw new ArgumentNullException("filterContext");
  178. }
  179. if (filterContext.IsChildAction) {
  180. ValidateChildActionConfiguration();
  181. // Already actively being captured? (i.e., cached child action inside of cached child action)
  182. // Realistically, this needs write substitution to do properly (including things like authentication)
  183. if (GetChildActionFilterFinishCallback(filterContext) != null) {
  184. throw new InvalidOperationException(MvcResources.OutputCacheAttribute_CannotNestChildCache);
  185. }
  186. // Already cached?
  187. string uniqueId = GetChildActionUniqueId(filterContext);
  188. string cachedValue = ChildActionCacheInternal.Get(uniqueId) as string;
  189. if (cachedValue != null) {
  190. filterContext.Result = new ContentResult() { Content = cachedValue };
  191. return;
  192. }
  193. // Swap in a new TextWriter so we can capture the output
  194. StringWriter cachingWriter = new StringWriter(CultureInfo.InvariantCulture);
  195. TextWriter originalWriter = filterContext.HttpContext.Response.Output;
  196. filterContext.HttpContext.Response.Output = cachingWriter;
  197. // Set a finish callback to clean up
  198. SetChildActionFilterFinishCallback(filterContext, wasException => {
  199. // Restore original writer
  200. filterContext.HttpContext.Response.Output = originalWriter;
  201. // Grab output and write it
  202. string capturedText = cachingWriter.ToString();
  203. filterContext.HttpContext.Response.Write(capturedText);
  204. // Only cache output if this wasn't an error
  205. if (!wasException) {
  206. ChildActionCacheInternal.Add(uniqueId, capturedText, DateTimeOffset.UtcNow.AddSeconds(Duration));
  207. }
  208. });
  209. }
  210. }
  211. public void OnException(ExceptionContext filterContext) {
  212. if (filterContext == null) {
  213. throw new ArgumentNullException("filterContext");
  214. }
  215. if (filterContext.IsChildAction) {
  216. CompleteChildAction(filterContext, wasException: true);
  217. }
  218. }
  219. public override void OnResultExecuting(ResultExecutingContext filterContext) {
  220. if (filterContext == null) {
  221. throw new ArgumentNullException("filterContext");
  222. }
  223. if (!filterContext.IsChildAction) {
  224. // we need to call ProcessRequest() since there's no other way to set the Page.Response intrinsic
  225. using (OutputCachedPage page = new OutputCachedPage(_cacheSettings)) {
  226. page.ProcessRequest(HttpContext.Current);
  227. }
  228. }
  229. }
  230. public override void OnResultExecuted(ResultExecutedContext filterContext) {
  231. if (filterContext == null) {
  232. throw new ArgumentNullException("filterContext");
  233. }
  234. if (filterContext.IsChildAction) {
  235. CompleteChildAction(filterContext, wasException: filterContext.Exception != null);
  236. }
  237. }
  238. private static void SetChildActionFilterFinishCallback(ControllerContext controllerContext, Action<bool> callback) {
  239. controllerContext.HttpContext.Items[_childActionFilterFinishCallbackKey] = callback;
  240. }
  241. private static IEnumerable<string> SplitVaryByParam(string varyByParam) {
  242. if (String.Equals(varyByParam, "none", StringComparison.OrdinalIgnoreCase)) { // Vary by nothing
  243. return Enumerable.Empty<string>();
  244. }
  245. if (String.Equals(varyByParam, "*", StringComparison.OrdinalIgnoreCase)) { // Vary by everything
  246. return null;
  247. }
  248. return from part in varyByParam.Split(';') // Vary by specific parameters
  249. let trimmed = part.Trim()
  250. where !String.IsNullOrEmpty(trimmed)
  251. select trimmed;
  252. }
  253. private void ValidateChildActionConfiguration() {
  254. if (Duration <= 0) {
  255. throw new InvalidOperationException(MvcResources.OutputCacheAttribute_InvalidDuration);
  256. }
  257. if (String.IsNullOrWhiteSpace(VaryByParam)) {
  258. throw new InvalidOperationException(MvcResources.OutputCacheAttribute_InvalidVaryByParam);
  259. }
  260. if (!String.IsNullOrWhiteSpace(CacheProfile) ||
  261. !String.IsNullOrWhiteSpace(SqlDependency) ||
  262. !String.IsNullOrWhiteSpace(VaryByContentEncoding) ||
  263. !String.IsNullOrWhiteSpace(VaryByHeader) ||
  264. _locationWasSet || _noStoreWasSet) {
  265. throw new InvalidOperationException(MvcResources.OutputCacheAttribute_ChildAction_UnsupportedSetting);
  266. }
  267. }
  268. private sealed class OutputCachedPage : Page {
  269. private OutputCacheParameters _cacheSettings;
  270. public OutputCachedPage(OutputCacheParameters cacheSettings) {
  271. // Tracing requires Page IDs to be unique.
  272. ID = Guid.NewGuid().ToString();
  273. _cacheSettings = cacheSettings;
  274. }
  275. protected override void FrameworkInitialize() {
  276. // when you put the <%@ OutputCache %> directive on a page, the generated code calls InitOutputCache() from here
  277. base.FrameworkInitialize();
  278. InitOutputCache(_cacheSettings);
  279. }
  280. }
  281. }
  282. }