Skip to content

Commit 42d4934

Browse files
author
DevTrends
committed
Added support for non-view ActionResults.
Added cache location support. Bug fix for using Html.Action overload without DonutOutputCacheAttribute. Changed code to encrypt donut hole settings by default.
1 parent 2530783 commit 42d4934

18 files changed

+272
-99
lines changed

DevTrends.MvcDonutCaching/ActionOutputBuilder.cs

Lines changed: 0 additions & 37 deletions
This file was deleted.

DevTrends.MvcDonutCaching/ActionSettingsSerialiser.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Runtime.Serialization;
33
using System.Text;
44
using System.Web.Routing;
5+
using System.Web.Script.Serialization;
56

67
namespace DevTrends.MvcDonutCaching
78
{
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using System.Web;
3+
using System.Web.UI;
4+
5+
namespace DevTrends.MvcDonutCaching
6+
{
7+
public class CacheHeadersHelper : ICacheHeadersHelper
8+
{
9+
public void SetCacheHeaders(HttpResponseBase response, CacheSettings settings)
10+
{
11+
var cacheability = HttpCacheability.NoCache;
12+
13+
switch (settings.Location)
14+
{
15+
case OutputCacheLocation.Any:
16+
case OutputCacheLocation.Downstream:
17+
cacheability = HttpCacheability.Public;
18+
break;
19+
case OutputCacheLocation.Client:
20+
case OutputCacheLocation.ServerAndClient:
21+
cacheability = HttpCacheability.Private;
22+
break;
23+
}
24+
25+
response.Cache.SetCacheability(cacheability);
26+
27+
if (cacheability != HttpCacheability.NoCache)
28+
{
29+
response.Cache.SetExpires(DateTime.Now.AddSeconds(settings.Duration));
30+
response.Cache.SetMaxAge(new TimeSpan(0, 0, settings.Duration));
31+
}
32+
}
33+
}
34+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+

2+
namespace DevTrends.MvcDonutCaching
3+
{
4+
public class CacheItem
5+
{
6+
public string Content { get; set; }
7+
public string ContentType { get; set; }
8+
}
9+
}
Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-

1+
using System.Web.UI;
2+
23
namespace DevTrends.MvcDonutCaching
34
{
45
public class CacheSettings
@@ -7,5 +8,16 @@ public class CacheSettings
78
public int Duration { get; set; }
89
public string VaryByParam { get; set; }
910
public string VaryByCustom { get; set; }
11+
public OutputCacheLocation Location { get; set; }
12+
13+
public bool IsServerCachingEnabled
14+
{
15+
get
16+
{
17+
return IsCachingEnabled && Duration > 0 && (Location == OutputCacheLocation.Any ||
18+
Location == OutputCacheLocation.Server ||
19+
Location == OutputCacheLocation.ServerAndClient);
20+
}
21+
}
1022
}
1123
}

DevTrends.MvcDonutCaching/DevTrends.MvcDonutCaching.csproj

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,18 @@
4747
<Reference Include="System.Xml" />
4848
</ItemGroup>
4949
<ItemGroup>
50-
<Compile Include="ActionOutputBuilder.cs" />
5150
<Compile Include="ActionSettings.cs" />
5251
<Compile Include="ActionSettingsSerialiser.cs" />
52+
<Compile Include="CacheHeadersHelper.cs" />
53+
<Compile Include="CacheItem.cs" />
5354
<Compile Include="CacheSettings.cs" />
5455
<Compile Include="CacheSettingsManager.cs" />
56+
<Compile Include="EncryptingActionSettingsSerialiser.cs" />
57+
<Compile Include="Encryptor.cs" />
58+
<Compile Include="ICacheHeadersHelper.cs" />
59+
<Compile Include="IEncryptor.cs" />
5560
<Compile Include="IExtendedOutputCacheManager.cs" />
5661
<Compile Include="DonutHoleFiller.cs" />
57-
<Compile Include="IActionOutputBuilder.cs" />
5862
<Compile Include="IActionSettingsSerialiser.cs" />
5963
<Compile Include="ICacheSettingsManager.cs" />
6064
<Compile Include="IDonutHoleFiller.cs" />

DevTrends.MvcDonutCaching/DonutHoleFiller.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace DevTrends.MvcDonutCaching
99
{
1010
public class DonutHoleFiller : IDonutHoleFiller
1111
{
12-
private static readonly Regex DonutHoles = new Regex("<!--Donut#(.*)#-->", RegexOptions.Compiled);
12+
private static readonly Regex DonutHoles = new Regex("<!--Donut#(.*?)#-->(.*?)<!--EndDonut-->", RegexOptions.Compiled | RegexOptions.Singleline);
1313

1414
private readonly IActionSettingsSerialiser _actionSettingsSerialiser;
1515

@@ -23,6 +23,16 @@ public DonutHoleFiller(IActionSettingsSerialiser actionSettingsSerialiser)
2323
_actionSettingsSerialiser = actionSettingsSerialiser;
2424
}
2525

26+
public string RemoveDonutHoleWrappers(string content, ControllerContext filterContext)
27+
{
28+
if (filterContext.IsChildAction)
29+
{
30+
return content;
31+
}
32+
33+
return DonutHoles.Replace(content, new MatchEvaluator(match => match.Groups[2].Value));
34+
}
35+
2636
public string ReplaceDonutHoleContent(string content, ControllerContext filterContext)
2737
{
2838
if (filterContext.IsChildAction)
Lines changed: 114 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
using System;
2+
using System.Globalization;
3+
using System.IO;
24
using System.Web.Mvc;
5+
using System.Web.UI;
6+
using System.Web;
37

48
namespace DevTrends.MvcDonutCaching
59
{
610
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
7-
public class DonutOutputCacheAttribute : ActionFilterAttribute
11+
public class DonutOutputCacheAttribute : ActionFilterAttribute, IExceptionFilter
812
{
13+
private const string CallbackKey = "d0nutCallback";
14+
915
private readonly IKeyGenerator _keyGenerator;
1016
private readonly IDonutHoleFiller _donutHoleFiller;
11-
private readonly IActionOutputBuilder _actionOutputBuilder;
1217
private readonly IExtendedOutputCacheManager _outputCacheManager;
1318
private readonly ICacheSettingsManager _cacheSettingsManager;
19+
private readonly ICacheHeadersHelper _cacheHeadersHelper;
1420

1521
private CacheSettings _cacheSettings;
1622
private string _cacheKey;
@@ -19,83 +25,152 @@ public class DonutOutputCacheAttribute : ActionFilterAttribute
1925
public string VaryByParam { get; set; }
2026
public string VaryByCustom { get; set; }
2127
public string CacheProfile { get; set; }
28+
public OutputCacheLocation Location { get; set; }
2229

2330
public DonutOutputCacheAttribute()
2431
{
2532
var keyBuilder = new KeyBuilder();
33+
2634
_keyGenerator = new KeyGenerator(keyBuilder);
27-
_donutHoleFiller = new DonutHoleFiller(new ActionSettingsSerialiser());
28-
_actionOutputBuilder = new ActionOutputBuilder();
35+
_donutHoleFiller = new DonutHoleFiller(new EncryptingActionSettingsSerialiser(new ActionSettingsSerialiser(), new Encryptor()));
2936
_outputCacheManager = new OutputCacheManager(OutputCache.Instance, keyBuilder);
3037
_cacheSettingsManager = new CacheSettingsManager();
31-
}
38+
_cacheHeadersHelper = new CacheHeadersHelper();
3239

33-
internal DonutOutputCacheAttribute(IKeyGenerator keyGenerator, IDonutHoleFiller donutHoleFiller, IActionOutputBuilder actionOutputBuilder,
34-
IExtendedOutputCacheManager outputCacheManager, ICacheSettingsManager cacheSettingsManager)
35-
{
36-
_keyGenerator = keyGenerator;
37-
_donutHoleFiller = donutHoleFiller;
38-
_actionOutputBuilder = actionOutputBuilder;
39-
_outputCacheManager = outputCacheManager;
40-
_cacheSettingsManager = cacheSettingsManager;
40+
Duration = -1;
41+
Location = (OutputCacheLocation)(-1);
4142
}
4243

4344
public override void OnActionExecuting(ActionExecutingContext filterContext)
4445
{
4546
_cacheSettings = BuildCacheSettings();
4647

47-
if (_cacheSettings.IsCachingEnabled)
48+
if (_cacheSettings.IsServerCachingEnabled)
4849
{
4950
_cacheKey = _keyGenerator.GenerateKey(filterContext, _cacheSettings);
5051

51-
var content = _outputCacheManager.GetItem(_cacheKey);
52+
var cachedItem = _outputCacheManager.GetItem(_cacheKey);
5253

53-
if (content != null)
54+
if (cachedItem != null)
5455
{
55-
filterContext.Result = new ContentResult { Content = _donutHoleFiller.ReplaceDonutHoleContent(content, filterContext) };
56+
filterContext.Result = new ContentResult
57+
{
58+
Content = _donutHoleFiller.ReplaceDonutHoleContent(cachedItem.Content, filterContext),
59+
ContentType = cachedItem.ContentType
60+
};
5661
}
5762
}
58-
}
5963

60-
public override void OnActionExecuted(ActionExecutedContext filterContext)
64+
if (filterContext.Result == null)
65+
{
66+
var callbackKey = BuildCallbackKey(filterContext);
67+
68+
var cachingWriter = new StringWriter(CultureInfo.InvariantCulture);
69+
70+
var originalWriter = filterContext.HttpContext.Response.Output;
71+
72+
filterContext.HttpContext.Response.Output = cachingWriter;
73+
74+
filterContext.HttpContext.Items[callbackKey] = new Action<bool>(hasErrors =>
75+
{
76+
filterContext.HttpContext.Items.Remove(callbackKey);
77+
78+
filterContext.HttpContext.Response.Output = originalWriter;
79+
80+
if (!hasErrors)
81+
{
82+
var cacheItem = new CacheItem
83+
{
84+
Content = cachingWriter.ToString(),
85+
ContentType = filterContext.HttpContext.Response.ContentType
86+
};
87+
88+
filterContext.HttpContext.Response.Write(_donutHoleFiller.RemoveDonutHoleWrappers(cacheItem.Content, filterContext));
89+
90+
if (_cacheSettings.IsServerCachingEnabled && filterContext.HttpContext.Response.StatusCode == 200)
91+
{
92+
_outputCacheManager.AddItem(_cacheKey, cacheItem, DateTime.Now.AddSeconds(_cacheSettings.Duration));
93+
}
94+
}
95+
});
96+
}
97+
}
98+
99+
public override void OnResultExecuted(ResultExecutedContext filterContext)
61100
{
62-
var viewResult = filterContext.Result as ViewResultBase; // only cache views and partials
101+
ExecuteCallback(filterContext, false);
63102

64-
if (viewResult != null)
103+
if (!filterContext.IsChildAction)
65104
{
66-
var content = _actionOutputBuilder.GetActionOutput(viewResult, filterContext);
105+
_cacheHeadersHelper.SetCacheHeaders(filterContext.HttpContext.Response, _cacheSettings);
106+
}
107+
}
67108

68-
if (_cacheSettings.IsCachingEnabled)
69-
{
70-
_outputCacheManager.AddItem(_cacheKey, content, DateTime.Now.AddSeconds(_cacheSettings.Duration));
71-
}
109+
public void OnException(ExceptionContext filterContext)
110+
{
111+
ExecuteCallback(filterContext, true);
112+
}
113+
114+
private void ExecuteCallback(ControllerContext context, bool hasErrors)
115+
{
116+
var callbackKey = BuildCallbackKey(context);
117+
118+
var callback = context.HttpContext.Items[callbackKey] as Action<bool>;
119+
120+
if (callback != null)
121+
{
122+
callback.Invoke(hasErrors);
123+
}
124+
}
125+
126+
private string BuildCallbackKey(ControllerContext context)
127+
{
128+
var actionName = context.RouteData.Values["action"].ToString();
129+
var controllerName = context.RouteData.Values["controller"].ToString();
72130

73-
filterContext.Result = new ContentResult { Content = _donutHoleFiller.ReplaceDonutHoleContent(content, filterContext) };
74-
}
131+
return string.Format("{0}.{1}.{2}", CallbackKey, controllerName, actionName);
75132
}
76133

77134
private CacheSettings BuildCacheSettings()
78135
{
136+
CacheSettings cacheSettings;
137+
79138
if (string.IsNullOrEmpty(CacheProfile))
80139
{
81-
return new CacheSettings
140+
cacheSettings = new CacheSettings
82141
{
83142
IsCachingEnabled = _cacheSettingsManager.IsCachingEnabledGlobally,
84143
Duration = Duration,
85144
VaryByCustom = VaryByCustom,
86-
VaryByParam = VaryByParam
87-
};
145+
VaryByParam = VaryByParam,
146+
Location = (int)Location == -1 ? OutputCacheLocation.Server : Location
147+
};
148+
}
149+
else
150+
{
151+
var cacheProfile = _cacheSettingsManager.RetrieveOutputCacheProfile(CacheProfile);
152+
153+
cacheSettings = new CacheSettings
154+
{
155+
IsCachingEnabled = _cacheSettingsManager.IsCachingEnabledGlobally && cacheProfile.Enabled,
156+
Duration = Duration == -1 ? cacheProfile.Duration : Duration,
157+
VaryByCustom = VaryByCustom ?? cacheProfile.VaryByCustom,
158+
VaryByParam = VaryByParam ?? cacheProfile.VaryByParam,
159+
Location = (int)Location == -1 ? ((int)cacheProfile.Location == -1 ? OutputCacheLocation.Server : cacheProfile.Location) : Location
160+
};
88161
}
89162

90-
var cacheProfile = _cacheSettingsManager.RetrieveOutputCacheProfile(CacheProfile);
163+
if (cacheSettings.Duration == -1)
164+
{
165+
throw new HttpException("The directive or the configuration settings profile must specify the 'duration' attribute.");
166+
}
91167

92-
return new CacheSettings
168+
if (cacheSettings.Duration < 0)
93169
{
94-
IsCachingEnabled = _cacheSettingsManager.IsCachingEnabledGlobally && cacheProfile.Enabled,
95-
Duration = Duration == 0 ? cacheProfile.Duration : Duration,
96-
VaryByCustom = VaryByCustom ?? cacheProfile.VaryByCustom,
97-
VaryByParam = VaryByParam ?? cacheProfile.VaryByParam
98-
};
99-
}
170+
throw new HttpException("The 'duration' attribute must have a value that is greater than or equal to zero.");
171+
}
172+
173+
return cacheSettings;
174+
}
100175
}
101176
}

0 commit comments

Comments
 (0)