Importing the Contrib.Cache module

--HG--
branch : 1.x
This commit is contained in:
Sebastien Ros
2013-05-24 17:16:57 -07:00
parent 06e53a3f36
commit 0d437b8060
31 changed files with 1911 additions and 0 deletions

View File

@@ -0,0 +1,19 @@
using Orchard.Localization;
using Orchard.Security;
using Orchard.UI.Navigation;
namespace Orchard.OutputCache {
public class AdminMenu : INavigationProvider {
public Localizer T { get; set; }
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder
.Add(T("Settings"), menu => menu
.Add(T("Cache"), "10.0", subMenu => subMenu.Action("Index", "Admin", new { area = "Orchard.OutputCache" }).Permission(StandardPermissions.SiteOwner)
.Add(T("Settings"), "10.0", item => item.Action("Index", "Admin", new { area = "Orchard.OutputCache" }).Permission(StandardPermissions.SiteOwner).LocalNav())
.Add(T("Statistics"), "10.0", item => item.Action("Index", "Statistics", new { area = "Orchard.OutputCache" }).Permission(StandardPermissions.SiteOwner).LocalNav())
));
}
}
}

View File

@@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Routing;
using Autofac.Features.Metadata;
using Orchard.OutputCache.Models;
using Orchard.OutputCache.Services;
using Orchard.OutputCache.ViewModels;
using Orchard;
using Orchard.Caching;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Mvc.Routes;
using Orchard.Security;
using Orchard.UI.Admin;
using Orchard.UI.Notify;
namespace Orchard.OutputCache.Controllers {
[Admin]
public class AdminController : Controller {
private readonly IEnumerable<Meta<IRouteProvider>> _routeProviders;
private readonly ISignals _signals;
private readonly ICacheService _cacheService;
public AdminController(
IOrchardServices services,
IEnumerable<Meta<IRouteProvider>> routeProviders,
ISignals signals,
ICacheService cacheService) {
_routeProviders = routeProviders;
_signals = signals;
_cacheService = cacheService;
Services = services;
}
public IOrchardServices Services { get; set; }
public Localizer T { get; set; }
public ActionResult Index() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
return new HttpUnauthorizedResult();
var routeConfigurations = new List<RouteConfiguration>();
var settings = Services.WorkContext.CurrentSite.As<CacheSettingsPart>();
foreach (var routeProvider in _routeProviders) {
// right now, ignore generic routes
if (routeProvider.Value is StandardExtensionRouteProvider) continue;
var routeCollection = routeProvider.Value.GetRoutes();
var feature = routeProvider.Metadata["Feature"] as Orchard.Environment.Extensions.Models.Feature;
// if there is no feature, skip route
if (feature == null) continue;
foreach (var routeDescriptor in routeCollection) {
var route = routeDescriptor.Route as Route;
if(route == null) {
continue;
}
// ignore admin routes
if (route.Url.StartsWith("Admin/") || route.Url == "Admin") continue;
var cacheParameterKey = _cacheService.GetRouteDescriptorKey(HttpContext, route);
var cacheParameter = _cacheService.GetCacheParameterByKey(cacheParameterKey);
var duration = cacheParameter == null ? default(int?) : cacheParameter.Duration;
routeConfigurations.Add(new RouteConfiguration {
RouteKey = cacheParameterKey,
Url = route.Url,
Priority = routeDescriptor.Priority,
Duration = duration,
FeatureName =
String.IsNullOrWhiteSpace(feature.Descriptor.Name)
? feature.Descriptor.Id
: feature.Descriptor.Name
});
}
}
var model = new IndexViewModel {
DefaultCacheDuration = settings.DefaultCacheDuration,
DefaultMaxAge = settings.DefaultMaxAge,
VaryQueryStringParameters = settings.VaryQueryStringParameters,
IgnoredUrls = settings.IgnoredUrls,
DebugMode = settings.DebugMode,
ApplyCulture = settings.ApplyCulture,
RouteConfigurations = routeConfigurations
};
return View(model);
}
[HttpPost, ActionName("Index")]
public ActionResult IndexPost() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
return new HttpUnauthorizedResult();
var model = new IndexViewModel {
RouteConfigurations = new List<RouteConfiguration>()
};
if(TryUpdateModel(model)) {
var settings = Services.WorkContext.CurrentSite.As<CacheSettingsPart>();
settings.DefaultCacheDuration = model.DefaultCacheDuration;
settings.DefaultMaxAge = model.DefaultMaxAge;
settings.VaryQueryStringParameters = model.VaryQueryStringParameters;
settings.IgnoredUrls = model.IgnoredUrls;
settings.DebugMode = model.DebugMode;
settings.ApplyCulture = model.ApplyCulture;
// invalidates the settings cache
_signals.Trigger(CacheSettingsPart.CacheKey);
_cacheService.SaveCacheConfigurations(model.RouteConfigurations);
Services.Notifier.Information(T("Cache Settings saved successfully."));
}
else {
Services.Notifier.Error(T("Could not save Cache Settings."));
}
return RedirectToAction("Index");
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Linq;
using System.Web.Mvc;
using Orchard.OutputCache.Services;
using Orchard.OutputCache.ViewModels;
using Orchard;
using Orchard.Environment.Configuration;
using Orchard.Localization;
using Orchard.Security;
using Orchard.Settings;
using Orchard.UI.Admin;
using Orchard.UI.Navigation;
namespace Orchard.OutputCache.Controllers {
[Admin]
public class StatisticsController : Controller {
private readonly ICacheService _cacheService;
private readonly IOutputCacheStorageProvider _cacheStorageProvider;
private readonly ShellSettings _shellSettings;
private readonly ISiteService _siteService;
public StatisticsController(
IOrchardServices services,
ICacheService cacheService,
IOutputCacheStorageProvider cacheStorageProvider,
ShellSettings shellSettings,
ISiteService siteService) {
_cacheService = cacheService;
_cacheStorageProvider = cacheStorageProvider;
_shellSettings = shellSettings;
_siteService = siteService;
Services = services;
}
public IOrchardServices Services { get; set; }
public Localizer T { get; set; }
public ActionResult Index(PagerParameters pagerParameters) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
return new HttpUnauthorizedResult();
var pager = new Pager(_siteService.GetSiteSettings(), pagerParameters);
var pagerShape = Services.New.Pager(pager).TotalItemCount(_cacheStorageProvider.GetCacheItemsCount());
var model = new StatisticsViewModel {
CacheItems = _cacheStorageProvider
.GetCacheItems(pager.GetStartIndex(), pager.PageSize)
.ToList(),
Pager = pagerShape
};
return View(model);
}
public ActionResult Evict(string cacheKey) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
return new HttpUnauthorizedResult();
_cacheStorageProvider.Remove(cacheKey);
return RedirectToAction("Index");
}
[HttpPost]
public ActionResult EvictAll() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
return new HttpUnauthorizedResult();
_cacheStorageProvider.RemoveAll();
return RedirectToAction("Index");
}
}
}

View File

@@ -0,0 +1,669 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.OutputCache.Models;
using Orchard.OutputCache.Services;
using Orchard;
using Orchard.Caching;
using Orchard.ContentManagement;
using Orchard.Environment.Configuration;
using Orchard.Logging;
using Orchard.Mvc.Filters;
using Orchard.Services;
using Orchard.Themes;
using Orchard.UI.Admin;
using Orchard.Utility.Extensions;
namespace Orchard.OutputCache.Filters
{
public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter
{
private readonly ICacheManager _cacheManager;
private readonly IOutputCacheStorageProvider _cacheStorageProvider;
private readonly ITagCache _tagCache;
private readonly IDisplayedContentItemHandler _displayedContentItemHandler;
private readonly IWorkContextAccessor _workContextAccessor;
private readonly IThemeManager _themeManager;
private readonly IClock _clock;
private readonly ICacheService _cacheService;
private readonly ISignals _signals;
private readonly ShellSettings _shellSettings;
private const string AntiforgeryBeacon = "<!--OutputCacheFilterAntiForgeryToken-->";
private const string AntiforgeryTag = "<input name=\"__RequestVerificationToken\" type=\"hidden\" value=\"";
private const string RefreshKey = "__r";
public OutputCacheFilter(
ICacheManager cacheManager,
IOutputCacheStorageProvider cacheStorageProvider,
ITagCache tagCache,
IDisplayedContentItemHandler displayedContentItemHandler,
IWorkContextAccessor workContextAccessor,
IThemeManager themeManager,
IClock clock,
ICacheService cacheService,
ISignals signals,
ShellSettings shellSettings)
{
_cacheManager = cacheManager;
_cacheStorageProvider = cacheStorageProvider;
_tagCache = tagCache;
_displayedContentItemHandler = displayedContentItemHandler;
_workContextAccessor = workContextAccessor;
_themeManager = themeManager;
_clock = clock;
_cacheService = cacheService;
_signals = signals;
_shellSettings = shellSettings;
Logger = NullLogger.Instance;
}
private bool _debugMode;
private int _cacheDuration;
private int _maxAge;
private string _ignoredUrls;
private bool _applyCulture;
private string _cacheKey;
private string _invariantCacheKey;
private string _actionName;
private DateTime _now;
private string[] _varyQueryStringParameters;
private WorkContext _workContext;
private CapturingResponseFilter _filter;
private CacheItem _cacheItem;
public ILogger Logger { get; set; }
public void OnActionExecuting(ActionExecutingContext filterContext)
{
// use the action in the cacheKey so that the same route can't return cache for different actions
_actionName = filterContext.ActionDescriptor.ActionName;
// apply OutputCacheAttribute logic if defined
var outputCacheAttribute = filterContext.ActionDescriptor.GetCustomAttributes(typeof (OutputCacheAttribute), true).Cast<OutputCacheAttribute>().FirstOrDefault() ;
if(outputCacheAttribute != null) {
if (outputCacheAttribute.Duration <= 0 || outputCacheAttribute.NoStore) {
Logger.Debug("Request ignored based on OutputCache attribute");
return;
}
}
// saving the current datetime
_now = _clock.UtcNow;
// before executing an action, we check if a valid cached result is already
// existing for this context (url, theme, culture, tenant)
Logger.Debug("Request on: " + filterContext.RequestContext.HttpContext.Request.RawUrl);
// don't cache POST requests
if(filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) ) {
Logger.Debug("Request ignored on POST");
return;
}
// don't cache the admin
if (AdminFilter.IsApplied(new RequestContext(filterContext.HttpContext, new RouteData()))) {
Logger.Debug("Request ignored on Admin section");
return;
}
// ignore child actions, e.g. HomeController is using RenderAction()
if (filterContext.IsChildAction){
Logger.Debug("Request ignored on Child actions");
return;
}
_workContext = _workContextAccessor.GetContext();
// don't return any cached content, or cache any content, if the user is authenticated
if (_workContext.CurrentUser != null) {
Logger.Debug("Request ignored on Authenticated user");
return;
}
// caches the default cache duration to prevent a query to the settings
_cacheDuration = _cacheManager.Get("CacheSettingsPart.Duration",
context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
return _workContext.CurrentSite.As<CacheSettingsPart>().DefaultCacheDuration;
}
);
// caches the default max age duration to prevent a query to the settings
_maxAge = _cacheManager.Get("CacheSettingsPart.MaxAge",
context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
return _workContext.CurrentSite.As<CacheSettingsPart>().DefaultMaxAge;
}
);
_varyQueryStringParameters = _cacheManager.Get("CacheSettingsPart.VaryQueryStringParameters",
context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
var varyQueryStringParameters = _workContext.CurrentSite.As<CacheSettingsPart>().VaryQueryStringParameters;
return string.IsNullOrWhiteSpace(varyQueryStringParameters) ? null
: varyQueryStringParameters.Split(new[]{","}, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
}
);
// caches the ignored urls to prevent a query to the settings
_ignoredUrls = _cacheManager.Get("CacheSettingsPart.IgnoredUrls",
context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
return _workContext.CurrentSite.As<CacheSettingsPart>().IgnoredUrls;
}
);
// caches the culture setting
_applyCulture = _cacheManager.Get("CacheSettingsPart.ApplyCulture",
context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
return _workContext.CurrentSite.As<CacheSettingsPart>().ApplyCulture;
}
);
// caches the ignored urls to prevent a query to the settings
_debugMode = _cacheManager.Get("CacheSettingsPart.DebugMode",
context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
return _workContext.CurrentSite.As<CacheSettingsPart>().DebugMode;
}
);
var queryString = filterContext.RequestContext.HttpContext.Request.QueryString;
var parameters = new Dictionary<string, object>(filterContext.ActionParameters);
foreach(var key in queryString.AllKeys) {
if (key == null) continue;
parameters[key] = queryString[key];
}
// compute the cache key
_cacheKey = ComputeCacheKey(filterContext, parameters);
// create a tag which doesn't care about querystring
_invariantCacheKey = ComputeCacheKey(filterContext, null);
// don't retrieve cache content if refused
// in this case the result of the action will update the current cached version
if (filterContext.RequestContext.HttpContext.Request.Headers["Cache-Control"] != "no-cache") {
// fetch cached data
_cacheItem = _cacheStorageProvider.GetCacheItem(_cacheKey);
if (_cacheItem == null) {
Logger.Debug("Cached version not found");
}
}
else {
Logger.Debug("Cache-Control = no-cache requested");
}
var response = filterContext.HttpContext.Response;
// render cached content
if (_cacheItem != null)
{
Logger.Debug("Cache item found, expires on " + _cacheItem.ValidUntilUtc);
var output = _cacheItem.Output;
/*
*
* There is no need to replace the AntiForgeryToken as it is not used for unauthenticated requests
* and at this point, the request can't be authenticated
*
*
// replace any anti forgery token with a fresh value
if (output.Contains(AntiforgeryBeacon))
{
var viewContext = new ViewContext
{
HttpContext = filterContext.HttpContext,
Controller = filterContext.Controller
};
var htmlHelper = new HtmlHelper(viewContext, new ViewDataContainer());
var siteSalt = _workContext.CurrentSite.SiteSalt;
var token = htmlHelper.AntiForgeryToken(siteSalt);
output = output.Replace(AntiforgeryBeacon, token.ToString());
}
*/
// adds some caching information to the output if requested
if (_debugMode)
{
output += "\r\n<!-- Cached on " + _cacheItem.CachedOnUtc + " (UTC) until " + _cacheItem.ValidUntilUtc + " (UTC) -->";
response.AddHeader("X-Cached-On", _cacheItem.CachedOnUtc.ToString("r"));
response.AddHeader("X-Cached-Until", _cacheItem.ValidUntilUtc.ToString("r"));
}
filterContext.Result = new ContentResult
{
Content = output,
ContentType = _cacheItem.ContentType
};
response.StatusCode = _cacheItem.StatusCode;
ApplyCacheControl(_cacheItem, response);
return;
}
_cacheItem = new CacheItem();
// get contents
ApplyCacheControl(_cacheItem, response);
// no cache content available, intercept the execution results for caching
response.Filter = _filter = new CapturingResponseFilter(response.Filter);
}
public void OnActionExecuted(ActionExecutedContext filterContext) {
// ignore error results from cache
if (filterContext.HttpContext.Response.StatusCode != (int)HttpStatusCode.OK) {
_filter = null;
return;
}
// if the result of a POST is a Redirect, remove any Cache Item for this url
// so that the redirected client gets a fresh result
// also add a random token to the query string so that public cachers (IIS, proxies, ...) don't return cached content
// i.e., Comment creation
// ignore in admin
if (AdminFilter.IsApplied(new RequestContext(filterContext.HttpContext, new RouteData()))) {
return;
}
_workContext = _workContextAccessor.GetContext();
// ignore authenticated requests
if (_workContext.CurrentUser != null) {
return;
}
if (filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)
&& filterContext.Result is RedirectResult) {
Logger.Debug("Redirect on POST");
var redirectUrl = ((RedirectResult)filterContext.Result).Url;
if (!VirtualPathUtility.IsAbsolute(redirectUrl)) {
var applicationRoot = filterContext.HttpContext.Request.ToRootUrlString();
if (redirectUrl.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) {
redirectUrl = redirectUrl.Substring(applicationRoot.Length);
}
}
// querystring invariant key
var invariantCacheKey = ComputeCacheKey(
_shellSettings.Name,
redirectUrl,
() => _workContext.CurrentCulture,
_themeManager.GetRequestTheme(filterContext.RequestContext).Id,
null
);
_cacheService.RemoveByTag(invariantCacheKey);
// adding a refresh key so that the next request will not be cached
var epIndex = redirectUrl.IndexOf('?');
var qs = new NameValueCollection();
if (epIndex > 0) {
qs = HttpUtility.ParseQueryString(redirectUrl.Substring(epIndex));
}
var refresh = _now.Ticks;
qs.Remove(RefreshKey);
qs.Add(RefreshKey, refresh.ToString("x"));
var querystring = "?" + string.Join("&", Array.ConvertAll(qs.AllKeys, k => string.Format("{0}={1}", HttpUtility.UrlEncode(k), HttpUtility.UrlEncode(qs[k]))));
if (epIndex > 0) {
redirectUrl = redirectUrl.Substring(0, epIndex) + querystring;
}
else {
redirectUrl = redirectUrl + querystring;
}
filterContext.Result = new RedirectResult(redirectUrl, ((RedirectResult)filterContext.Result).Permanent);
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
}
}
public void OnResultExecuted(ResultExecutedContext filterContext)
{
var response = filterContext.HttpContext.Response;
// save the result only if the content can be intercepted
if (_filter == null) return;
// only for ViewResult right now, as we don't want to handle redirects, HttpNotFound, ...
if (filterContext.Result as ViewResultBase == null) {
Logger.Debug("Ignoring none ViewResult response");
return;
}
// check if there is a specific rule not to cache the whole route
var configurations = _cacheService.GetRouteConfigurations();
var route = filterContext.Controller.ControllerContext.RouteData.Route;
var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
var configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
// do not cache ?
if (configuration != null && configuration.Duration == 0)
{
return;
}
// ignored url ?
if (IsIgnoredUrl(filterContext.RequestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath, _ignoredUrls))
{
return;
}
if(response.IsClientConnected)
response.Flush();
var output = _filter.GetContents(response.ContentEncoding);
if (String.IsNullOrWhiteSpace(output))
{
return;
}
var tokenIndex = output.IndexOf(AntiforgeryTag, StringComparison.Ordinal);
// substitute antiforgery token by a beacon
if (tokenIndex != -1)
{
var tokenEnd = output.IndexOf(">", tokenIndex, StringComparison.Ordinal);
var sb = new StringBuilder();
sb.Append(output.Substring(0, tokenIndex));
sb.Append(AntiforgeryBeacon);
sb.Append(output.Substring(tokenEnd + 1));
output = sb.ToString();
}
// default duration of specific one ?
var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : _cacheDuration;
// include each of the content item ids as tags for the cache entry
var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
_cacheItem.ContentType = response.ContentType;
_cacheItem.StatusCode = response.StatusCode;
_cacheItem.CachedOnUtc = _now;
_cacheItem.ValidFor = cacheDuration;
_cacheItem.QueryString = filterContext.HttpContext.Request.Url.Query;
_cacheItem.Output = output;
_cacheItem.CacheKey = _cacheKey;
_cacheItem.InvariantCacheKey = _invariantCacheKey;
_cacheItem.Tenant = _shellSettings.Name;
_cacheItem.Url = filterContext.HttpContext.Request.Url.AbsolutePath;
_cacheItem.Tags = new[] { _invariantCacheKey }.Union(contentItemIds).ToArray();
Logger.Debug("Cache item added: " + _cacheItem.CacheKey);
// remove old cache data
_cacheService.RemoveByTag(_invariantCacheKey);
// add data to cache
_cacheStorageProvider.Set(_cacheKey, _cacheItem);
// add to the tags index
foreach (var tag in _cacheItem.Tags) {
_tagCache.Tag(tag, _cacheKey);
}
}
public void OnResultExecuting(ResultExecutingContext filterContext)
{
}
/// <summary>
/// Define valid cache control values
/// </summary>
private void ApplyCacheControl(CacheItem cacheItem, HttpResponseBase response) {
if (_maxAge > 0) {
var maxAge = new TimeSpan(0, 0, 0, _maxAge); //cacheItem.ValidUntilUtc - _clock.UtcNow;
if (maxAge.TotalMilliseconds < 0) {
maxAge = TimeSpan.FromSeconds(0);
}
response.Cache.SetCacheability(HttpCacheability.Public);
response.Cache.SetMaxAge(maxAge);
}
// an ETag is a string that uniquely identifies a specific version of a component.
// we use the cache item to detect if it's a new one
response.Cache.SetETag(cacheItem.GetHashCode().ToString(CultureInfo.InvariantCulture));
response.Cache.SetOmitVaryStar(true);
if (_varyQueryStringParameters != null) {
foreach (var queryStringParam in _varyQueryStringParameters) {
response.Cache.VaryByParams[queryStringParam] = true;
}
}
// different tenants with the same urls have different entries
response.Cache.VaryByHeaders["HOST"] = true;
// Set the Vary: Accept-Encoding response header.
// This instructs the proxies to cache two versions of the resource: one compressed, and one uncompressed.
// The correct version of the resource is delivered based on the client request header.
// This is a good choice for applications that are singly homed and depend on public proxies for user locality.
response.Cache.VaryByHeaders["Accept-Encoding"] = true;
// create a unique cache per browser, in case a Theme is rendered differently (e.g., mobile)
// c.f. http://msdn.microsoft.com/en-us/library/aa478965.aspx
// c.f. http://stackoverflow.com/questions/6007287/outputcache-varybyheader-user-agent-or-varybycustom-browser
response.Cache.SetVaryByCustom("browser");
// enabling this would create an entry for each different browser sub-version
// response.Cache.VaryByHeaders.UserAgent = true;
}
private string ComputeCacheKey(ControllerContext controllerContext, IEnumerable<KeyValuePair<string, object>> parameters) {
var url = controllerContext.HttpContext.Request.RawUrl;
if(!VirtualPathUtility.IsAbsolute(url)) {
var applicationRoot = controllerContext.HttpContext.Request.ToRootUrlString();
if(url.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) {
url = url.Substring(applicationRoot.Length);
}
}
return ComputeCacheKey(_shellSettings.Name, url, () => _workContext.CurrentCulture, _themeManager.GetRequestTheme(controllerContext.RequestContext).Id, parameters);
}
private string ComputeCacheKey(string tenant, string absoluteUrl, Func<string> culture, string theme, IEnumerable<KeyValuePair<string, object>> parameters){
var keyBuilder = new StringBuilder();
keyBuilder.Append("tenant=").Append(tenant).Append(";");
keyBuilder.Append("url=").Append(absoluteUrl.ToLowerInvariant()).Append(";");
// include the theme in the cache key
if (_applyCulture) {
keyBuilder.Append("culture=").Append(culture().ToLowerInvariant()).Append(";");
}
// include the theme in the cache key
keyBuilder.Append("theme=").Append(theme.ToLowerInvariant()).Append(";");
// include the theme in the cache key
keyBuilder.Append("action=").Append(_actionName.ToLowerInvariant()).Append(";");
if (parameters != null) {
foreach (var pair in parameters) {
keyBuilder.AppendFormat("{0}={1};", pair.Key.ToLowerInvariant(), Convert.ToString(pair.Value).ToLowerInvariant());
}
}
return keyBuilder.ToString();
}
/// <summary>
/// Returns true if the given url should be ignored, as defined in the settings
/// </summary>
private static bool IsIgnoredUrl(string url, string ignoredUrls)
{
if(String.IsNullOrEmpty(ignoredUrls))
{
return false;
}
// remove ~ if present
if(url.StartsWith("~")) {
url = url.Substring(1);
}
using (var urlReader = new StringReader(ignoredUrls))
{
string relativePath;
while (null != (relativePath = urlReader.ReadLine()))
{
// remove ~ if present
if (relativePath.StartsWith("~")) {
relativePath = relativePath.Substring(1);
}
if (String.IsNullOrWhiteSpace(relativePath))
{
continue;
}
relativePath = relativePath.Trim();
// ignore comments
if(relativePath.StartsWith("#"))
{
continue;
}
if(String.Equals(relativePath, url, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
}
}
/// <summary>
/// Captures the response stream while writing to it
/// </summary>
public class CapturingResponseFilter : Stream
{
private readonly Stream _sink;
private readonly MemoryStream _mem;
public CapturingResponseFilter(Stream sink)
{
_sink = sink;
_mem = new MemoryStream();
}
// The following members of Stream must be overriden.
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return false; }
}
public override long Length
{
get { return 0; }
}
public override long Position { get; set; }
public override long Seek(long offset, SeekOrigin direction)
{
return 0;
}
public override void SetLength(long length)
{
_sink.SetLength(length);
}
public override void Close()
{
_sink.Close();
_mem.Close();
}
public override void Flush()
{
_sink.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return _sink.Read(buffer, offset, count);
}
// Override the Write method to filter Response to a file.
public override void Write(byte[] buffer, int offset, int count)
{
//Here we will not write to the sink b/c we want to capture
_sink.Write(buffer, offset, count);
//Write out the response to the file.
_mem.Write(buffer, 0, count);
}
public string GetContents(Encoding enc)
{
var buffer = new byte[_mem.Length];
_mem.Position = 0;
_mem.Read(buffer, 0, buffer.Length);
return enc.GetString(buffer, 0, buffer.Length);
}
}
public class ViewDataContainer : IViewDataContainer
{
public ViewDataDictionary ViewData { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
using System.Globalization;
using Orchard.OutputCache.Models;
using Orchard.OutputCache.Services;
using Orchard.ContentManagement;
using Orchard.Core.Common.Models;
using Orchard.Data;
using Orchard.ContentManagement.Handlers;
namespace Orchard.OutputCache.Handlers {
public class CacheSettingsPartHandler : ContentHandler {
private readonly ICacheService _cacheService;
public CacheSettingsPartHandler(
IRepository<CacheSettingsPartRecord> repository,
ICacheService cacheService) {
_cacheService = cacheService;
Filters.Add(new ActivatingFilter<CacheSettingsPart>("Site"));
Filters.Add(StorageFilter.For(repository));
// initializing default cache settings values
OnInitializing<CacheSettingsPart>((context, part) => { part.DefaultCacheDuration = 300; });
// evict modified routable content when updated
OnPublished<IContent>((context, part) => Invalidate(part));
}
private void Invalidate(IContent content) {
// remove any page tagged with this content item id
_cacheService.RemoveByTag(content.ContentItem.Id.ToString(CultureInfo.InvariantCulture));
// search the cache for containers too
var commonPart = content.As<CommonPart>();
if (commonPart != null) {
if (commonPart.Container != null) {
_cacheService.RemoveByTag(commonPart.Container.Id.ToString(CultureInfo.InvariantCulture));
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Orchard.OutputCache.Services;
using Orchard.ContentManagement.Handlers;
namespace Orchard.OutputCache.Handlers {
/// <summary>
/// Saves references to content items which have been displayed during a request
/// </summary>
public class DisplayedContentItemHandler : ContentHandler, IDisplayedContentItemHandler {
private readonly Collection<int> _itemIds = new Collection<int>();
protected override void BuildDisplayShape(BuildDisplayContext context) {
_itemIds.Add(context.Content.Id);
}
public bool IsDisplayed(int id) {
return _itemIds.Contains(id);
}
public IEnumerable<int> GetDisplayed() {
return _itemIds.Distinct();
}
}
}

View File

@@ -0,0 +1,84 @@
using Orchard.Data.Migration;
namespace Orchard.OutputCache {
public class Migrations : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("CacheSettingsPartRecord",
table => table
.ContentPartRecord()
.Column<int>("DefaultCacheDuration")
.Column<int>("DefaultMaxAge")
.Column<string>("IgnoredUrls", c => c.Unlimited())
.Column<bool>("DebugMode", c => c.WithDefault(false))
.Column<bool>("ApplyCulture", c => c.WithDefault(false))
);
SchemaBuilder.CreateTable("CacheParameterRecord",
table => table
.Column<int>("Id", c => c.PrimaryKey().Identity())
.Column<int>("Duration")
.Column<int>("MaxAge")
.Column<string>("RouteKey", c => c.WithLength(255))
);
return 4;
}
public int UpdateFrom1() {
SchemaBuilder.CreateTable("CacheParameterRecord",
table => table
.Column<int>("Id", c => c.PrimaryKey().Identity())
.Column<int>("Duration")
.Column<string>("RouteKey", c => c.WithLength(255))
);
SchemaBuilder.AlterTable("CacheSettingsPartRecord",
table => table
.AddColumn<string>("IgnoredUrls", c => c.Unlimited())
);
SchemaBuilder.AlterTable("CacheSettingsPartRecord",
table => table
.AddColumn<bool>("DebugMode", c => c.WithDefault(false))
);
return 2;
}
public int UpdateFrom2() {
SchemaBuilder.AlterTable("CacheSettingsPartRecord",
table => table
.AddColumn<bool>("ApplyCulture", c => c.WithDefault(false))
);
return 3;
}
public int UpdateFrom3() {
SchemaBuilder.AlterTable("CacheSettingsPartRecord",
table => table
.AddColumn<int>("DefaultMaxAge")
);
SchemaBuilder.AlterTable("CacheParameterRecord",
table => table
.AddColumn<int>("MaxAge")
);
return 4;
}
public int UpdateFrom4() {
SchemaBuilder.AlterTable("CacheSettingsPartRecord",
table => table
.AddColumn<string>("VaryQueryStringParameters", c => c.Unlimited())
);
return 5;
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
namespace Orchard.OutputCache.Models {
[Serializable]
public class CacheItem {
public int ValidFor { get; set; }
public DateTime CachedOnUtc { get; set; }
public string Output { get; set; }
public string ContentType { get; set; }
public string QueryString { get; set; }
public string CacheKey { get; set; }
public string InvariantCacheKey { get; set; }
public string Url { get; set; }
public string Tenant { get; set; }
public int StatusCode { get; set; }
public string[] Tags { get; set; }
public DateTime ValidUntilUtc {
get { return CachedOnUtc.AddSeconds(ValidFor); }
}
}
}

View File

@@ -0,0 +1,11 @@
namespace Orchard.OutputCache.Models
{
public class CacheParameterRecord
{
public virtual int Id { get; set; }
public virtual string RouteKey { get; set; }
public virtual int Duration { get; set; }
public virtual int MaxAge { get; set; }
}
}

View File

@@ -0,0 +1,37 @@
using Orchard.ContentManagement;
namespace Orchard.OutputCache.Models {
public class CacheSettingsPart : ContentPart<CacheSettingsPartRecord> {
public const string CacheKey = "CacheSettingsPart";
public int DefaultCacheDuration {
get { return Record.DefaultCacheDuration; }
set { Record.DefaultCacheDuration = value; }
}
public int DefaultMaxAge {
get { return Record.DefaultMaxAge; }
set { Record.DefaultMaxAge = value; }
}
public string VaryQueryStringParameters {
get { return Record.VaryQueryStringParameters; }
set { Record.VaryQueryStringParameters = value; }
}
public string IgnoredUrls {
get { return Record.IgnoredUrls; }
set { Record.IgnoredUrls = value; }
}
public bool ApplyCulture {
get { return Record.ApplyCulture; }
set { Record.ApplyCulture = value; }
}
public bool DebugMode {
get { return Record.DebugMode; }
set { Record.DebugMode = value; }
}
}
}

View File

@@ -0,0 +1,17 @@
using Orchard.ContentManagement.Records;
using Orchard.Data.Conventions;
namespace Orchard.OutputCache.Models {
public class CacheSettingsPartRecord : ContentPartRecord {
public virtual int DefaultCacheDuration { get; set; }
public virtual int DefaultMaxAge { get; set; }
public virtual bool DebugMode { get; set; }
public virtual bool ApplyCulture { get; set; }
[StringLengthMax]
public virtual string VaryQueryStringParameters { get; set; }
[StringLengthMax]
public virtual string IgnoredUrls { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
Name: Output Cache
AntiForgery: enabled
Author: Sébastien Ros
Website: http://orchardproject.net/
Version: 1.0
OrchardVersion: 1.0
Description: Adds Output Caching functionality.
Features:
Orchard.OutputCache:
Description: Adds Output Caching functionality.
Category: Performance

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}</ProjectGuid>
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Orchard.OutputCache</RootNamespace>
<AssemblyName>Orchard.OutputCache</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<MvcBuildViews>false</MvcBuildViews>
<FileUpgradeFlags>
</FileUpgradeFlags>
<OldToolsVersion>4.0</OldToolsVersion>
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<TargetFrameworkProfile />
<UseIISExpress>false</UseIISExpress>
<IISExpressSSLPort />
<IISExpressAnonymousAuthentication />
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="Autofac, Version=2.5.2.830, Culture=neutral, PublicKeyToken=17863af14b0044da, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\autofac\Autofac.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CSharp" />
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.ComponentModel.DataAnnotations">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Web.DynamicData" />
<Reference Include="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
</Reference>
<Reference Include="System.Web" />
<Reference Include="System.Web.Abstractions" />
<Reference Include="System.Web.Routing" />
<Reference Include="System.Xml" />
<Reference Include="System.Configuration" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Content Include="Web.config" />
<Content Include="Views\Web.config" />
<Content Include="Scripts\Web.config" />
<Content Include="Styles\Web.config" />
<Content Include="Properties\AssemblyInfo.cs" />
<Content Include="Module.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Orchard\Orchard.Framework.csproj">
<Project>{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}</Project>
<Name>Orchard.Framework</Name>
</ProjectReference>
<ProjectReference Include="..\..\Core\Orchard.Core.csproj">
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
<Name>Orchard.Core</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Controllers\StatisticsController.cs" />
<Compile Include="Filters\OutputCacheFilter.cs" />
<Compile Include="Handlers\CacheSettingsPartHandler.cs" />
<Compile Include="Handlers\DisplayedContentItemHandler.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Models\CacheItem.cs" />
<Compile Include="Models\CacheSettingsPart.cs" />
<Compile Include="Models\CacheSettingsPartRecord.cs" />
<Compile Include="Models\CacheParameterRecord.cs" />
<Compile Include="Services\CacheService.cs" />
<Compile Include="Services\DefaultTagCache.cs" />
<Compile Include="Services\IDisplayedContentItemHandler.cs" />
<Compile Include="Services\ITagCache.cs" />
<Compile Include="Services\DefaultCacheStorageProvider.cs" />
<Compile Include="Services\ICacheService.cs" />
<Compile Include="Services\IOutputCacheStorageProvider.cs" />
<Compile Include="ViewModels\StatisticsViewModel.cs" />
<Compile Include="ViewModels\IndexViewModel.cs" />
<Compile Include="ViewModels\RouteConfiguration.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Statistics\Index.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\Index.cshtml" />
</ItemGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target> -->
<Target Name="AfterBuild" DependsOnTargets="AfterBuildCompiler">
<PropertyGroup>
<AreasManifestDir>$(ProjectDir)\..\Manifests</AreasManifestDir>
</PropertyGroup>
<!-- If this is an area child project, uncomment the following line:
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Child" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
-->
<!-- If this is an area parent project, uncomment the following lines:
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Parent" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
<CopyAreaManifests ManifestPath="$(AreasManifestDir)" CrossCopy="false" RenameViews="true" />
-->
</Target>
<Target Name="AfterBuildCompiler" Condition="'$(MvcBuildViews)'=='true'">
<AspNetCompiler VirtualPath="temp" PhysicalPath="$(ProjectDir)\..\$(ProjectName)" />
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>45979</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://orchard.codeplex.com</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@@ -0,0 +1,34 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Orchard.OutputCache")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyProduct("Orchard")]
[assembly: AssemblyCopyright("Copyright <20> Outercurve Foundation 2013")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("cc5ee19a-8a16-49cf-96a8-5a1bcdd4fa7d")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Revision and Build Numbers
// by using the '*' as shown below:
[assembly: AssemblyVersion("1.0")]
[assembly: AssemblyFileVersion("1.0")]

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.web>
<httpHandlers>
<!-- iis6 - for any request in this location, return via managed static file handler -->
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
</httpHandlers>
</system.web>
<system.webServer>
<handlers accessPolicy="Script,Read">
<!--
iis7 - for any request to a file exists on disk, return it via native http module.
accessPolicy 'Script' is to allow for a managed 404 page.
-->
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
</handlers>
</system.webServer>
</configuration>

View File

@@ -0,0 +1,121 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Routing;
using Orchard.OutputCache.Models;
using Orchard.OutputCache.ViewModels;
using Orchard;
using Orchard.Caching;
using Orchard.Data;
using Orchard.Utility.Extensions;
namespace Orchard.OutputCache.Services {
public class CacheService : ICacheService {
private readonly IWorkContextAccessor _workContextAccessor;
private readonly IRepository<CacheParameterRecord> _repository;
private readonly ICacheManager _cacheManager;
private readonly IOutputCacheStorageProvider _cacheStorageProvider;
private readonly ITagCache _tagCache;
private readonly ISignals _signals;
public CacheService(
IWorkContextAccessor workContextAccessor,
IRepository<CacheParameterRecord> repository,
ICacheManager cacheManager,
IOutputCacheStorageProvider cacheStorageProvider,
ITagCache tagCache,
ISignals signals) {
_workContextAccessor = workContextAccessor;
_repository = repository;
_cacheManager = cacheManager;
_cacheStorageProvider = cacheStorageProvider;
_tagCache = tagCache;
_signals = signals;
}
public void RemoveByTag(string tag) {
Collection<string> itemKeys;
if (_tagCache.TryGetValue(tag, out itemKeys)) {
foreach (var key in itemKeys) {
_cacheStorageProvider.Remove(key);
}
}
}
public IEnumerable<CacheItem> GetCacheItems() {
var workContext = _workContextAccessor.GetContext();
foreach (DictionaryEntry cacheEntry in workContext.HttpContext.Cache) {
var cacheItem = cacheEntry.Value as CacheItem;
if (cacheItem != null) {
yield return cacheItem;
}
}
}
public void Evict(string cacheKey) {
var workContext = _workContextAccessor.GetContext();
workContext.HttpContext.Cache.Remove(cacheKey);
}
public string GetRouteDescriptorKey(HttpContextBase httpContext, RouteBase routeBase) {
var route = routeBase as Route;
var dataTokens = route != null ? route.DataTokens : routeBase.GetRouteData(httpContext).DataTokens;
var keyBuilder = new StringBuilder();
if (route != null) {
keyBuilder.AppendFormat("url={0};", route.Url);
}
// the data tokens are used in case the same url is used by several features, like *{path} (Rewrite Rules and Home Page Provider)
if (dataTokens != null) {
foreach (var key in dataTokens.Keys) {
keyBuilder.AppendFormat("{0}={1};", key, dataTokens[key]);
}
}
return keyBuilder.ToString().ToLowerInvariant();
}
public CacheParameterRecord GetCacheParameterByKey(string key) {
return _repository.Get(c => c.RouteKey == key);
}
public IEnumerable<RouteConfiguration> GetRouteConfigurations() {
return _cacheManager.Get("GetRouteConfigurations",
ctx => {
ctx.Monitor(_signals.When("GetRouteConfigurations"));
return _repository.Fetch(c => true).Select(c => new RouteConfiguration { RouteKey = c.RouteKey, Duration = c.Duration }).ToReadOnlyCollection();
});
}
public void SaveCacheConfigurations(IEnumerable<RouteConfiguration> routeConfigurations) {
// remove all current configurations
var configurations = _repository.Fetch(c => true);
foreach (var configuration in configurations) {
_repository.Delete(configuration);
}
// save the new configurations
foreach (var configuration in routeConfigurations) {
if (!configuration.Duration.HasValue) {
continue;
}
_repository.Create(new CacheParameterRecord {
Duration = configuration.Duration.Value,
RouteKey = configuration.RouteKey
});
}
// invalidate the cache
_signals.Trigger("GetRouteConfigurations");
}
}
}

View File

@@ -0,0 +1,66 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Orchard.OutputCache.Models;
using Orchard;
using Orchard.Environment.Configuration;
namespace Orchard.OutputCache.Services {
public class DefaultCacheStorageProvider : IOutputCacheStorageProvider {
private readonly string _tenantName;
private readonly WorkContext _workContext;
public DefaultCacheStorageProvider(IWorkContextAccessor workContextAccessor, ShellSettings shellSettings) {
_workContext = workContextAccessor.GetContext();
_tenantName = shellSettings.Name;
}
public void Set(string key, CacheItem cacheItem) {
_workContext.HttpContext.Cache.Add(
key,
cacheItem,
null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
new TimeSpan(0, 0, cacheItem.ValidFor),
System.Web.Caching.CacheItemPriority.Normal,
null);
}
public void Remove(string key) {
_workContext.HttpContext.Cache.Remove(key);
}
public void RemoveAll() {
var items = GetCacheItems(0, 100).ToList();
while (items.Any()) {
foreach (var item in items) {
Remove(item.CacheKey);
}
}
}
public CacheItem GetCacheItem(string key) {
return _workContext.HttpContext.Cache.Get(key) as CacheItem;
}
public IEnumerable<CacheItem> GetCacheItems(int skip, int count) {
// the ASP.NET cache can also contain other types of items
return _workContext.HttpContext.Cache.AsParallel()
.Cast<DictionaryEntry>()
.Select(x => x.Value)
.OfType<CacheItem>()
.Where(x => x.Tenant.Equals(_tenantName, StringComparison.OrdinalIgnoreCase))
.Skip(skip)
.Take(count);
}
public int GetCacheItemsCount() {
return _workContext.HttpContext.Cache.AsParallel()
.Cast<DictionaryEntry>()
.Select(x => x.Value)
.OfType<CacheItem>()
.Count(x => x.Tenant.Equals(_tenantName, StringComparison.OrdinalIgnoreCase));
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using Orchard.OutputCache.Models;
namespace Orchard.OutputCache.Services {
/// <summary>
/// Tenant wide case insensitive reverse index for <see cref="CacheItem"/> tags.
/// </summary>
public class DefaultTagCache : ConcurrentDictionary<string, Collection<string>>, ITagCache {
public DefaultTagCache() : base(StringComparer.OrdinalIgnoreCase) {
}
public void Tag(string tag, params string[] keys) {
var collection = GetOrAdd(tag, x => new Collection<string>());
foreach (var key in keys) {
if (!collection.Contains(key)) {
collection.Add(key);
}
}
}
}
}

View File

@@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.Web;
using System.Web.Routing;
using Orchard.OutputCache.ViewModels;
using Orchard;
using Orchard.OutputCache.Models;
namespace Orchard.OutputCache.Services {
public interface ICacheService : IDependency {
/// <summary>
/// Returns the parameters for a specific route
/// </summary>
/// <param name="key">The key representing the route</param>
/// <returns>A <see cref="CacheParameterRecord"/> instance for the specified key, or <c>null</c></returns>
CacheParameterRecord GetCacheParameterByKey(string key);
/// <summary>
/// Removes all cache entries associated with a specific tag.
/// </summary>
/// <param name="tag">The tag value.</param>
void RemoveByTag(string tag);
/// <summary>
/// Returns the key representing a specific route in the db
/// </summary>
string GetRouteDescriptorKey(HttpContextBase httpContext, RouteBase route);
/// <summary>
/// Saves a set of <see cref="RouteConfiguration"/> to the database
/// </summary>
/// <param name="routeConfigurations"></param>
void SaveCacheConfigurations(IEnumerable<RouteConfiguration> routeConfigurations);
/// <summary>
/// Returns all defined configurations for specific routes
/// </summary>
IEnumerable<RouteConfiguration> GetRouteConfigurations();
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
using Orchard;
namespace Orchard.OutputCache.Services {
public interface IDisplayedContentItemHandler : IDependency {
bool IsDisplayed(int id);
IEnumerable<int> GetDisplayed();
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Generic;
using Orchard.OutputCache.Models;
using Orchard;
namespace Orchard.OutputCache.Services {
public interface IOutputCacheStorageProvider : IDependency {
/// <summary>
/// Adds a new <see cref="CacheItem"/> or substitute it with a new one if the
/// cache key is already used.
/// </summary>
/// <param name="key">The unique key representing the <see cref="CacheItem"/>.</param>
/// <param name="cacheItem">The <see cref="CacheItem"/> instance to add to the cache.</param>
void Set(string key, CacheItem cacheItem);
void Remove(string key);
void RemoveAll();
CacheItem GetCacheItem(string key);
IEnumerable<CacheItem> GetCacheItems(int skip, int count);
int GetCacheItemsCount();
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Orchard;
namespace Orchard.OutputCache.Services {
public interface ITagCache : IDictionary<string, Collection<string>>, ISingletonDependency {
void Tag(string tag, params string[] keys);
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.web>
<httpHandlers>
<!-- iis6 - for any request in this location, return via managed static file handler -->
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
</httpHandlers>
</system.web>
<system.webServer>
<handlers accessPolicy="Script,Read">
<!--
iis7 - for any request to a file exists on disk, return it via native http module.
accessPolicy 'Script' is to allow for a managed 404 page.
-->
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
</handlers>
</system.webServer>
</configuration>

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Orchard.OutputCache.ViewModels {
public class IndexViewModel {
public List<RouteConfiguration> RouteConfigurations { get; set; }
[Range(0, int.MaxValue), Required]
public int DefaultCacheDuration { get; set; }
[Range(0, int.MaxValue), Required]
public int DefaultMaxAge { get; set; }
public string IgnoredUrls { get; set; }
public bool ApplyCulture { get; set; }
public bool DebugMode { get; set; }
public string VaryQueryStringParameters { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace Orchard.OutputCache.ViewModels {
public class RouteConfiguration {
public string RouteKey { get; set; }
public string Url { get; set; }
public int Priority { get; set; }
public int? Duration { get; set; }
public int? MaxAge { get; set; }
public string FeatureName { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
using Orchard.OutputCache.Models;
namespace Orchard.OutputCache.ViewModels {
public class StatisticsViewModel {
public IEnumerable<CacheItem> CacheItems { get; set; }
public dynamic Pager { get; set; }
}
}

View File

@@ -0,0 +1,78 @@
@model Orchard.OutputCache.ViewModels.IndexViewModel
@{
Layout.Title = T("Cache Settings");
// group configurations by feature name
var featureRouteConfigurations = Model.RouteConfigurations
.GroupBy(x => x.FeatureName)
.ToDictionary(x => x.Key, x => x.Select(y => y));
}
@using (Html.BeginFormAntiForgeryPost())
{
@Html.ValidationSummary()
<fieldset>
<label>@T("Default Cache Duration")</label>
@Html.TextBoxFor(m => m.DefaultCacheDuration, new { @class = "text-small" })
<span class="hint">@T("Number of seconds the pages should be kept in cache on the server.")</span>
</fieldset>
<fieldset>
<label>@T("Max Age")</label>
@Html.TextBoxFor(m => m.DefaultMaxAge, new { @class = "text-small" })
<span class="hint">@T("When defined, a cache-control header with a max-age property will be added. Use this in order to enable kernel cache on IIS.")</span>
</fieldset>
<fieldset>
<label>@T("Vary Query String Parameters")</label>
@Html.TextBoxFor(m => m.VaryQueryStringParameters, new { @class = "textMedium" })
<span class="hint">@T("When defined, using comma separated values, sets caching to vary via specified query string parameters")</span>
</fieldset>
<fieldset>
<label>@T("Ignored urls")</label>
@Html.TextAreaFor(m => m.IgnoredUrls, new { @class = "textMedium" })
<span class="hint">@T("This must be a set of relative paths, e.g., /, /About. Please ensure that you enter only one path per line.")</span>
<span class="hint">@T("You can add comments by starting the line with #.")</span>
</fieldset>
<fieldset>
<label>@T("Culture")</label>
@Html.CheckBoxFor(m => m.ApplyCulture) <label for="@Html.FieldIdFor(m => m.ApplyCulture)" class="forcheckbox">@T("Differentiate cultures")</label>
<span class="hint">@T("When checked, the cached content will differ per culture too. For performance reasons leave it unchecked if you website uses only one culture.")</span>
</fieldset>
<fieldset>
<label>@T("Debug mode")</label>
@Html.CheckBoxFor(m => m.DebugMode) <label for="@Html.FieldIdFor(m => m.DebugMode)" class="forcheckbox">@T("Render caching information in cached pages")</label>
</fieldset>
foreach (var feature in featureRouteConfigurations.Keys) {
<h2>@T(feature)</h2>
<table class="items">
<thead>
<tr>
<th scope="col">@T("Route")</th>
<th scope="col">@T("Priority")</th>
<th scope="col">@T("Duration")</th>
</tr>
</thead>
@foreach (var routeConfiguration in featureRouteConfigurations[feature]) {
var index = Model.RouteConfigurations.IndexOf(routeConfiguration);
<tr>
<td>@routeConfiguration.Url</td>
<td style="width:60px;">@routeConfiguration.Priority</td>
<td style="width:500px;">
@Html.TextBoxFor(m => m.RouteConfigurations[index].Duration, new { @class = "text-small" })
<span class="hint">@T("Leave empty to use default duration, 0 to disable caching on this route.")</span>
</td>
@Html.HiddenFor(m => m.RouteConfigurations[index].RouteKey)
</tr>
}
</table>
}
<button class="primaryAction" type="submit">@T("Save")</button>
}

View File

@@ -0,0 +1,35 @@
@using Orchard.OutputCache.ViewModels
@model StatisticsViewModel
@{
Layout.Title = T("Cache Statistics");
}
@using (Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<div class="manage">@Html.ActionLink(T("Evict All").ToString(), "EvictAll", new { Area = "Orchard.OutputCache", Controller = "Statistics" }, new { @class = "button primaryAction", itemprop = "UnsafeUrl" })</div>
}
<h2>@T("Cached pages")</h2>
<table class="items">
<thead>
<tr>
<th scope="col">@T("Url")</th>
<th scope="col">@T("Cached On")</th>
<th scope="col">@T("Cached Until")</th>
<th scope="col">&nbsp;</th>
</tr>
</thead>
@foreach (var cacheItem in Model.CacheItems) {
<tr>
<td><span title="@cacheItem.QueryString">@cacheItem.Url</span></td>
<td>@Display.DateTimeRelative(dateTimeUtc: cacheItem.CachedOnUtc)</td>
<td>@cacheItem.ValidUntilUtc.ToLocalTime()</td>
<td>@Html.ActionLink(T("Evict").Text, "Evict", new { Area = "Orchard.OutputCache", Controller = "Statistics", cacheKey = cacheItem.CacheKey })</td>
</tr>
}
</table>
@Display(Model.Pager)

View File

@@ -0,0 +1,41 @@
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.web>
<httpHandlers>
</httpHandlers>
<!--
Enabling request validation in view pages would cause validation to occur
after the input has already been processed by the controller. By default
MVC performs request validation before a controller processes the input.
To change this behavior apply the ValidateInputAttribute to a
controller or action.
-->
<pages
validateRequest="false"
pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"
pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"
userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<controls>
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" namespace="System.Web.Mvc" tagPrefix="mvc" />
</controls>
</pages>
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<handlers>
</handlers>
</system.webServer>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="2.0.0.0" newVersion="3.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<remove name="host" />
<remove name="pages" />
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
</sectionGroup>
</configSections>
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<pages pageBaseType="Orchard.Mvc.ViewEngines.Razor.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="System.Linq"/>
<add namespace="System.Web.WebPages" />
<add namespace="System.Collections.Generic"/>
<add namespace="Orchard.Mvc.Html"/>
</namespaces>
</pages>
</system.web.webPages.razor>
<system.web>
<compilation targetFramework="4.0">
<assemblies>
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</assemblies>
</compilation>
</system.web>
</configuration>

View File

@@ -156,6 +156,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.ImageEditor", "Orch
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.MediaLibrary", "Orchard.Web\Modules\Orchard.MediaLibrary\Orchard.MediaLibrary.csproj", "{73A7688A-5BD3-4F7E-ADFA-CE36C5A10E3B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.OutputCache", "Orchard.Web\Modules\Orchard.OutputCache\Orchard.OutputCache.csproj", "{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
CodeCoverage|Any CPU = CodeCoverage|Any CPU
@@ -874,6 +876,16 @@ Global
{73A7688A-5BD3-4F7E-ADFA-CE36C5A10E3B}.FxCop|Any CPU.Build.0 = Release|Any CPU
{73A7688A-5BD3-4F7E-ADFA-CE36C5A10E3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{73A7688A-5BD3-4F7E-ADFA-CE36C5A10E3B}.Release|Any CPU.Build.0 = Release|Any CPU
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Coverage|Any CPU.Build.0 = Release|Any CPU
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.FxCop|Any CPU.ActiveCfg = Release|Any CPU
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.FxCop|Any CPU.Build.0 = Release|Any CPU
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -934,6 +946,7 @@ Global
{E649EA64-D213-461B-87F7-D67035801443} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{1F0B6B85-8B0B-47CA-899D-F25B4F1B52C3} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{73A7688A-5BD3-4F7E-ADFA-CE36C5A10E3B} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{ABC826D4-2FA1-4F2F-87DE-E6095F653810} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{F112851D-B023-4746-B6B1-8D2E5AD8F7AA} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
{6CB3EB30-F725-45C0-9742-42599BA8E8D2} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}