Various improvements to OutputCache module.

- Added synchronization of multiple concurrent content regeneration on cache miss.
- Added support for grace time, configurable per route.
- Refactored and cleaned up code and comments througout the module.
- Made many members "protected virtual" on OutputCacheFilter for easier substitution.
- Made various usability improvements in admin UI.
- Added support for "vary by authentication state".
This commit is contained in:
Daniel Stolt
2014-12-04 19:41:05 +08:00
parent d7edef9bb8
commit 6340e504bc
21 changed files with 701 additions and 605 deletions

View File

@@ -3,14 +3,14 @@ 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.OutputCache.Models;
using Orchard.OutputCache.Services;
using Orchard.OutputCache.ViewModels;
using Orchard.Security;
using Orchard.UI.Admin;
using Orchard.UI.Notify;
@@ -37,42 +37,43 @@ namespace Orchard.OutputCache.Controllers {
public Localizer T { get; set; }
public ActionResult Index() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You do not have permission to manage output cache.")))
return new HttpUnauthorizedResult();
var routeConfigurations = new List<RouteConfiguration>();
var routeConfigs = new List<CacheRouteConfig>();
var settings = Services.WorkContext.CurrentSite.As<CacheSettingsPart>();
foreach (var routeProvider in _routeProviders) {
// right now, ignore generic routes
// Right now, ignore generic routes.
if (routeProvider.Value is StandardExtensionRouteProvider) continue;
var routeCollection = routeProvider.Value.GetRoutes();
var routes = routeProvider.Value.GetRoutes();
var feature = routeProvider.Metadata["Feature"] as Orchard.Environment.Extensions.Models.Feature;
// if there is no feature, skip route
// If there is no feature, skip route.
if (feature == null) continue;
foreach (var routeDescriptor in routeCollection) {
foreach (var routeDescriptor in routes) {
var route = routeDescriptor.Route as Route;
if(route == null) {
continue;
}
// ignore admin routes
// 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;
var graceTime = cacheParameter == null ? default(int?) : cacheParameter.GraceTime;
routeConfigurations.Add(new RouteConfiguration {
routeConfigs.Add(new CacheRouteConfig {
RouteKey = cacheParameterKey,
Url = route.Url,
Priority = routeDescriptor.Priority,
Duration = duration,
GraceTime = graceTime,
FeatureName =
String.IsNullOrWhiteSpace(feature.Descriptor.Name)
? feature.Descriptor.Id
@@ -82,16 +83,18 @@ namespace Orchard.OutputCache.Controllers {
}
var model = new IndexViewModel {
RouteConfigs = routeConfigs,
DefaultCacheDuration = settings.DefaultCacheDuration,
DefaultCacheGraceTime = settings.DefaultCacheGraceTime,
DefaultMaxAge = settings.DefaultMaxAge,
VaryQueryStringParameters = settings.VaryQueryStringParameters,
VaryRequestHeaders = settings.VaryRequestHeaders,
VaryByQueryStringParameters = settings.VaryByQueryStringParameters,
VaryByRequestHeaders = settings.VaryByRequestHeaders,
IgnoredUrls = settings.IgnoredUrls,
DebugMode = settings.DebugMode,
ApplyCulture = settings.ApplyCulture,
RouteConfigurations = routeConfigurations,
IgnoreNoCache = settings.IgnoreNoCache,
CacheAuthenticatedRequests = settings.CacheAuthenticatedRequests
VaryByCulture = settings.VaryByCulture,
CacheAuthenticatedRequests = settings.CacheAuthenticatedRequests,
VaryByAuthenticationState = settings.VaryByAuthenticationState,
DebugMode = settings.DebugMode
};
return View(model);
@@ -99,33 +102,35 @@ namespace Orchard.OutputCache.Controllers {
[HttpPost, ActionName("Index")]
public ActionResult IndexPost() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You do not have permission to manage output cache.")))
return new HttpUnauthorizedResult();
var model = new IndexViewModel {
RouteConfigurations = new List<RouteConfiguration>()
RouteConfigs = new List<CacheRouteConfig>()
};
if(TryUpdateModel(model)) {
var settings = Services.WorkContext.CurrentSite.As<CacheSettingsPart>();
settings.DefaultCacheDuration = model.DefaultCacheDuration;
settings.DefaultCacheGraceTime = model.DefaultCacheGraceTime;
settings.DefaultMaxAge = model.DefaultMaxAge;
settings.VaryQueryStringParameters = model.VaryQueryStringParameters;
settings.VaryRequestHeaders = model.VaryRequestHeaders;
settings.VaryByQueryStringParameters = model.VaryByQueryStringParameters;
settings.VaryByRequestHeaders = model.VaryByRequestHeaders;
settings.IgnoredUrls = model.IgnoredUrls;
settings.DebugMode = model.DebugMode;
settings.ApplyCulture = model.ApplyCulture;
settings.IgnoreNoCache = model.IgnoreNoCache;
settings.VaryByCulture = model.VaryByCulture;
settings.CacheAuthenticatedRequests = model.CacheAuthenticatedRequests;
settings.VaryByAuthenticationState = model.VaryByAuthenticationState;
settings.DebugMode = model.DebugMode;
// invalidates the settings cache
_signals.Trigger(CacheSettingsPart.CacheKey);
_cacheService.SaveCacheConfigurations(model.RouteConfigurations);
// Invalidate the settings cache.
_signals.Trigger(CacheSettings.CacheKey);
_cacheService.SaveRouteConfigs(model.RouteConfigs);
Services.Notifier.Information(T("Cache Settings saved successfully."));
Services.Notifier.Information(T("Output cache settings saved successfully."));
}
else {
Services.Notifier.Error(T("Could not save Cache Settings."));
Services.Notifier.Error(T("Could not save output cache settings."));
}
return RedirectToAction("Index");

View File

@@ -36,7 +36,7 @@ namespace Orchard.OutputCache.Controllers {
public Localizer T { get; set; }
public ActionResult Index(PagerParameters pagerParameters) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You do not have permission to manage output cache.")))
return new HttpUnauthorizedResult();
var pager = new Pager(_siteService.GetSiteSettings(), pagerParameters);
@@ -53,7 +53,7 @@ namespace Orchard.OutputCache.Controllers {
}
public ActionResult Evict(string cacheKey) {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You do not have permission to manage output cache.")))
return new HttpUnauthorizedResult();
_cacheStorageProvider.Remove(cacheKey);
@@ -63,7 +63,7 @@ namespace Orchard.OutputCache.Controllers {
[HttpPost]
public ActionResult EvictAll() {
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("You do not have permission to manage output cache.")))
return new HttpUnauthorizedResult();
_cacheStorageProvider.RemoveAll();

View File

@@ -8,28 +8,42 @@ namespace Contrib.Cache.Database {
public class DatabaseOutputCacheMigrations : DataMigrationImpl {
public int Create() {
// Creating table CacheItemRecord
SchemaBuilder.CreateTable("CacheItemRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<int>("ValidFor")
.Column<DateTime>("ValidUntilUtc")
// Creating table CacheItemRecord
SchemaBuilder.CreateTable("CacheItemRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<DateTime>("CachedOnUtc")
.Column<string>("Output", column => column.Unlimited())
.Column<int>("Duration")
.Column<int>("GraceTime", c => c.Nullable())
.Column<DateTime>("ValidUntilUtc")
.Column<DateTime>("StoredUntilUtc")
.Column<string>("Output", column => column.Unlimited())
.Column<string>("ContentType")
.Column<string>("QueryString", column => column.WithLength(2048))
.Column<string>("CacheKey", column => column.WithLength(2048))
.Column<string>("InvariantCacheKey", column => column.WithLength(2048))
.Column<string>("Url", column => column.WithLength(2048))
.Column<string>("Tenant")
.Column<int>("StatusCode")
.Column<int>("StatusCode")
.Column<string>("Tags", column => column.Unlimited())
);
);
SchemaBuilder.AlterTable("CacheItemRecord", table => table
.CreateIndex("IDX_CacheItemRecord_CacheKey", "CacheKey")
);
return 1;
return 2;
}
public int UpdateFrom1() {
SchemaBuilder.AlterTable("CacheItemRecord",
table => {
table.DropColumn("ValidFor");
table.AddColumn<int>("Duration");
table.AddColumn<int>("GraceTime", c => c.Nullable());
table.AddColumn<DateTime>("StoredUntilUtc");
});
return 2;
}
}
}

View File

@@ -1,34 +1,38 @@
using System;
using System.Collections.Concurrent;
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.Threading;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.Mvc.Extensions;
using Orchard.OutputCache.Models;
using Orchard.OutputCache.Services;
using Orchard.Caching;
using Orchard.ContentManagement;
using Orchard.Environment.Configuration;
using Orchard.Logging;
using Orchard.Mvc.Extensions;
using Orchard.Mvc.Filters;
using Orchard.OutputCache.Models;
using Orchard.OutputCache.Services;
using Orchard.OutputCache.ViewModels;
using Orchard.Services;
using Orchard.Themes;
using Orchard.UI.Admin;
using Orchard.UI.Notify;
using Orchard.Utility.Extensions;
using System.Collections.Specialized;
using Orchard.OutputCache.ViewModels;
using Orchard.UI.Admin.Notification;
using Orchard.DisplayManagement.Shapes;
namespace Orchard.OutputCache.Filters {
public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter {
private static string _refreshKey = "__r";
private static long _epoch = new DateTime(2014, DateTimeKind.Utc).Ticks;
private static readonly ConcurrentDictionary<string, object> _cacheKeyLocks = new ConcurrentDictionary<string, object>();
// Dependencies.
private readonly ICacheManager _cacheManager;
private readonly IOutputCacheStorageProvider _cacheStorageProvider;
private readonly ITagCache _tagCache;
@@ -39,13 +43,7 @@ namespace Orchard.OutputCache.Filters {
private readonly ICacheService _cacheService;
private readonly ISignals _signals;
private readonly ShellSettings _shellSettings;
private readonly ICacheControlStrategy _cacheControlStrategy;
TextWriter _originalWriter;
StringWriter _cachingWriter;
private static string RefreshKey = "__r";
private static long Epoch = new DateTime(2014, DateTimeKind.Utc).Ticks;
public ILogger Logger { get; set; }
public OutputCacheFilter(
ICacheManager cacheManager,
@@ -57,8 +55,7 @@ namespace Orchard.OutputCache.Filters {
IClock clock,
ICacheService cacheService,
ISignals signals,
ShellSettings shellSettings,
ICacheControlStrategy cacheControlStrategy) {
ShellSettings shellSettings) {
_cacheManager = cacheManager;
_cacheStorageProvider = cacheStorageProvider;
@@ -70,340 +67,305 @@ namespace Orchard.OutputCache.Filters {
_cacheService = cacheService;
_signals = signals;
_shellSettings = shellSettings;
_cacheControlStrategy = cacheControlStrategy;
Logger = NullLogger.Instance;
}
private bool _debugMode;
private int _cacheDuration;
private int _maxAge;
private string _ignoredUrls;
private bool _applyCulture;
private bool _ignoreNoCache;
// State.
private CacheSettings _cacheSettings;
private DateTime _now;
private WorkContext _workContext;
private string _cacheKey;
private string _invariantCacheKey;
private DateTime _now;
private string[] _varyQueryStringParameters;
private ISet<string> _varyRequestHeaders;
private bool _transformRedirect;
private bool _cacheAuthenticatedRequests;
private WorkContext _workContext;
private CacheItem _cacheItem;
private Func<ControllerContext, string> _completeResponse;
public ILogger Logger { get; set; }
private Func<ControllerContext, string> _completeResponseFunc;
public void OnActionExecuting(ActionExecutingContext filterContext) {
// apply OutputCacheAttribute logic if defined
var actionAttributes = filterContext.ActionDescriptor.GetCustomAttributes(typeof(OutputCacheAttribute), true);
var controllerAttributes = filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(OutputCacheAttribute), true);
var outputCacheAttribute = actionAttributes.Concat(controllerAttributes).Cast<OutputCacheAttribute>().FirstOrDefault();
Logger.Debug("Incoming request for URL '{0}'.", filterContext.RequestContext.HttpContext.Request.RawUrl);
_workContext = _workContextAccessor.GetContext();
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;
}
// cache if authenticated setting
_cacheAuthenticatedRequests = _cacheManager.Get("CacheSettingsPart.CacheAuthenticatedRequests", context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
return _workContext.CurrentSite.As<CacheSettingsPart>().CacheAuthenticatedRequests;
});
// don't return any cached content, or cache any content, if the user is authenticated, unless the setting "cache authenticated requests" is true
if (_workContext.CurrentUser != null && !_cacheAuthenticatedRequests) {
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 cache duration to prevent a query to the settings
_ignoreNoCache = _cacheManager.Get("CacheSettingsPart.IgnoreNoCache",
context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
return _workContext.CurrentSite.As<CacheSettingsPart>().IgnoreNoCache;
}
);
// caches the default max age duration to prevent a query to the settings
_maxAge = GetMaxAge();
_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();
}
);
var varyRequestHeadersFromSettings = _cacheManager.Get("CacheSettingsPart.VaryRequestHeaders",
context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
var varyRequestHeaders = _workContext.CurrentSite.As<CacheSettingsPart>().VaryRequestHeaders;
return string.IsNullOrWhiteSpace(varyRequestHeaders) ? null
: varyRequestHeaders.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
}
);
_varyRequestHeaders = (varyRequestHeadersFromSettings == null) ? new HashSet<string>() : new HashSet<string>(varyRequestHeadersFromSettings);
// different tenants with the same urls have different entries
_varyRequestHeaders.Add("HOST");
// 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 debug mode
_debugMode = _cacheManager.Get("CacheSettingsPart.DebugMode",
context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
return _workContext.CurrentSite.As<CacheSettingsPart>().DebugMode;
}
);
// don't cache ignored url ?
if (IsIgnoredUrl(filterContext.RequestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath, _ignoredUrls)) {
return;
}
var queryString = filterContext.RequestContext.HttpContext.Request.QueryString;
var requestHeaders = filterContext.RequestContext.HttpContext.Request.Headers;
var parameters = new Dictionary<string, object>(filterContext.ActionParameters);
foreach (var key in queryString.AllKeys) {
if (key == null || (_varyQueryStringParameters != null
&& !_varyQueryStringParameters.Contains(key))) continue;
// ignore pages with the RefreshKey
if (String.Equals(RefreshKey, key, StringComparison.OrdinalIgnoreCase)) {
return;
}
parameters[key] = queryString[key];
}
foreach (var varyByRequestHeader in _varyRequestHeaders) {
if (requestHeaders.AllKeys.Contains(varyByRequestHeader)) {
parameters["HEADER:" + varyByRequestHeader] = requestHeaders[varyByRequestHeader];
}
}
// compute the cache key
_cacheKey = ComputeCacheKey(filterContext, parameters);
// create a tag which doesn't care about querystring
_workContext = _workContextAccessor.GetContext();
_cacheKey = ComputeCacheKey(filterContext, GetCacheKeyParameters(filterContext));
_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" || _ignoreNoCache) {
// 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;
// adds some caching information to the output if requested
if (_debugMode) {
response.AddHeader("X-Cached-On", _cacheItem.CachedOnUtc.ToString("r"));
response.AddHeader("X-Cached-Until", _cacheItem.ValidUntilUtc.ToString("r"));
}
// shorcut action execution
filterContext.Result = new ContentResult {
Content = output,
ContentType = _cacheItem.ContentType
};
response.StatusCode = _cacheItem.StatusCode;
ApplyCacheControl(_cacheItem, response);
Logger.Debug("Cache key '{0}' was created.", _cacheKey);
if (!RequestIsCacheable(filterContext))
return;
// The cache key lock for a given cache key is used to synchronize requests to
// ensure only a single request is regenerating the item.
var cacheKeyLock = _cacheKeyLocks.GetOrAdd(_cacheKey, x => new object());
try {
// Is there a cached item, and are we allowed to serve it?
var allowServeFromCache = filterContext.RequestContext.HttpContext.Request.Headers["Cache-Control"] != "no-cache" || CacheSettings.IgnoreNoCache;
var cacheItem = _cacheStorageProvider.GetCacheItem(_cacheKey);
if (allowServeFromCache && cacheItem != null) {
Logger.Debug("Item '{0}' was found in cache.", _cacheKey);
// Is the cached item in its grace period?
if (cacheItem.IsInGracePeriod(_now)) {
// Render the content unless another request is already doing so.
if (Monitor.TryEnter(cacheKeyLock)) {
Logger.Debug("Item '{0}' is in grace period and not currently being rendered; rendering item...", _cacheKey);
BeginRenderItem(filterContext);
return;
}
}
// Cached item is not yet in its grace period, or is already being
// rendered by another request; serve it from cache.
Logger.Debug("Serving item '{0}' from cache.", _cacheKey);
ServeCachedItem(filterContext, cacheItem);
return;
}
// No cached item found, or client doesn't want it; acquire the cache key
// lock to render the item.
Logger.Debug("Item '{0}' was not found in cache or client refuses it. Acquiring cache key lock...", _cacheKey);
if (Monitor.TryEnter(cacheKeyLock, TimeSpan.FromSeconds(20))) {
Logger.Debug("Cache key lock for item '{0}' was acquired.", _cacheKey);
// Item might now have been rendered and cached by another request; if so serve it from cache.
if (allowServeFromCache) {
cacheItem = _cacheStorageProvider.GetCacheItem(_cacheKey);
if (cacheItem != null) {
Logger.Debug("Item '{0}' was now found; releasing cache key lock and serving from cache.", _cacheKey);
Monitor.Exit(cacheKeyLock);
ServeCachedItem(filterContext, cacheItem);
return;
}
}
}
// Either we acquired the cache key lock and the item was still not in cache, or
// the lock acquisition timed out. In either case render the item.
Logger.Debug("Rendering item '{0}'...", _cacheKey);
BeginRenderItem(filterContext);
}
catch {
// Remember to release the cache key lock in the event of an exception!
Logger.Debug("Exception occurred for item '{0}'; releasing any acquired lock.", _cacheKey);
if (Monitor.IsEntered(cacheKeyLock))
Monitor.Exit(cacheKeyLock);
throw;
}
_cacheItem = new CacheItem();
// get contents
ApplyCacheControl(_cacheItem, response);
// no cache content available, intercept the execution results for caching, using the targetted encoding
_originalWriter = filterContext.HttpContext.Response.Output;
_cachingWriter = new StringWriterWithEncoding(_originalWriter.Encoding, _originalWriter.FormatProvider);
filterContext.HttpContext.Response.Output = _cachingWriter;
_completeResponse = CaptureResponse;
}
public void OnActionExecuted(ActionExecutedContext filterContext) {
// handle redirections
_transformRedirect = TransformRedirect(filterContext);
}
public void OnResultExecuting(ResultExecutingContext filterContext) {
}
public void OnResultExecuted(ResultExecutedContext filterContext) {
string capturedResponse = null;
if (_completeResponse != null) {
capturedResponse = _completeResponse(filterContext);
try {
string renderedOutput = null;
if (_completeResponseFunc != null) {
renderedOutput = _completeResponseFunc(filterContext);
}
if (renderedOutput == null) {
return;
}
Logger.Debug("Item '{0}' was rendered.", _cacheKey);
// Obtain individual route configuration, if any.
CacheRouteConfig configuration = null;
var configurations = _cacheService.GetRouteConfigs();
if (configurations.Any()) {
var route = filterContext.Controller.ControllerContext.RouteData.Route;
var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
}
if (!ResponseIsCacheable(filterContext, configuration)) {
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
filterContext.HttpContext.Response.Cache.SetNoStore();
filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
return;
}
// Determine duration and grace time.
var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : CacheSettings.DefaultCacheDuration;
var cacheGraceTime = configuration != null && configuration.GraceTime.HasValue ? configuration.GraceTime.Value : CacheSettings.DefaultCacheGraceTime;
// Include each content item ID as tags for the cache entry.
var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
var response = filterContext.HttpContext.Response;
var cacheItem = new CacheItem() {
CachedOnUtc = _now,
Duration = cacheDuration,
GraceTime = cacheGraceTime,
Output = renderedOutput,
ContentType = response.ContentType,
QueryString = filterContext.HttpContext.Request.Url.Query,
CacheKey = _cacheKey,
InvariantCacheKey = _invariantCacheKey,
Url = filterContext.HttpContext.Request.Url.AbsolutePath,
Tenant = _shellSettings.Name,
StatusCode = response.StatusCode,
Tags = new[] { _invariantCacheKey }.Union(contentItemIds).ToArray()
};
// Write the rendered item to the cache.
_cacheStorageProvider.Remove(_cacheKey);
_cacheStorageProvider.Set(_cacheKey, cacheItem);
Logger.Debug("Item '{0}' was written to cache.", _cacheKey);
// Also add the item tags to the tag cache.
foreach (var tag in cacheItem.Tags) {
_tagCache.Tag(tag, _cacheKey);
}
}
finally {
// Always release the cache key lock when the request ends.
if (_cacheKey != null) {
object cacheKeyLock;
if (_cacheKeyLocks.TryGetValue(_cacheKey, out cacheKeyLock) && Monitor.IsEntered(cacheKeyLock)) {
Logger.Debug("Releasing cache key lock for item '{0}'.", _cacheKey);
Monitor.Exit(cacheKeyLock);
}
}
}
}
protected virtual bool RequestIsCacheable(ActionExecutingContext filterContext) {
// Respect OutputCacheAttribute if applied.
var actionAttributes = filterContext.ActionDescriptor.GetCustomAttributes(typeof(OutputCacheAttribute), true);
var controllerAttributes = filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(OutputCacheAttribute), true);
var outputCacheAttribute = actionAttributes.Concat(controllerAttributes).Cast<OutputCacheAttribute>().FirstOrDefault();
if (outputCacheAttribute != null) {
if (outputCacheAttribute.Duration <= 0 || outputCacheAttribute.NoStore) {
Logger.Debug("Request for item '{0}' ignored based on OutputCache attribute.", _cacheKey);
return false;
}
}
var response = filterContext.HttpContext.Response;
// ignore error results from cache
if (response.StatusCode != (int)HttpStatusCode.OK ||
_transformRedirect) {
// Never cache non-200 responses.
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
filterContext.HttpContext.Response.Cache.SetNoStore();
filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
return;
// Don't cache POST requests.
if (filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)) {
Logger.Debug("Request for item '{0}' ignored because HTTP method is POST.", _cacheKey);
return false;
}
if (capturedResponse == null) {
return;
// Don't cache admin section requests.
if (AdminFilter.IsApplied(new RequestContext(filterContext.HttpContext, new RouteData()))) {
Logger.Debug("Request for item '{0}' ignored because it's in admin section.", _cacheKey);
return false;
}
// check if there is a specific rule not to cache the whole route
RouteConfiguration configuration = null;
var configurations = _cacheService.GetRouteConfigurations();
if (configurations.Any()) {
var route = filterContext.Controller.ControllerContext.RouteData.Route;
var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
// Ignore child actions, e.g. HomeController is using RenderAction()
if (filterContext.IsChildAction) {
Logger.Debug("Request for item '{0}' ignored because it's a child action.", _cacheKey);
return false;
}
// do not cache ?
if (configuration != null && configuration.Duration == 0) {
return;
// Ignore authenticated requests unless the setting to cache them is true.
if (_workContext.CurrentUser != null && !CacheSettings.CacheAuthenticatedRequests) {
Logger.Debug("Request for item '{0}' ignored because user is authenticated.", _cacheKey);
return false;
}
// don't cache the result if there were some notifications
var hasNotifications = !String.IsNullOrEmpty(Convert.ToString(filterContext.Controller.TempData["messages"]));
if (hasNotifications) {
Logger.Debug("Not caching: notifications present");
return;
// Don't cache ignored URLs.
if (IsIgnoredUrl(filterContext.RequestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath, CacheSettings.IgnoredUrls)) {
Logger.Debug("Request for item '{0}' ignored because the URL is configured as ignored.", _cacheKey);
return false;
}
// default duration of specific one ?
var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : _cacheDuration;
// Ignore requests with the refresh key on the query string.
foreach (var key in filterContext.RequestContext.HttpContext.Request.QueryString.AllKeys) {
if (String.Equals(_refreshKey, key, StringComparison.OrdinalIgnoreCase)) {
Logger.Debug("Request for item '{0}' ignored because refresh key was found on query string.", _cacheKey);
return false;
}
}
// include each of the content item ids as tags for the cache entry
var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
return true;
}
protected virtual bool ResponseIsCacheable(ResultExecutedContext filterContext, CacheRouteConfig configuration) {
if (filterContext.HttpContext.Request.Url == null) {
return;
return false;
}
_cacheItem.ContentType = response.ContentType;
_cacheItem.StatusCode = response.StatusCode;
_cacheItem.CachedOnUtc = _now;
_cacheItem.ValidFor = cacheDuration;
_cacheItem.QueryString = filterContext.HttpContext.Request.Url.Query;
_cacheItem.Output = capturedResponse;
_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);
// update the cached data
_cacheStorageProvider.Remove(_cacheKey);
_cacheStorageProvider.Set(_cacheKey, _cacheItem);
// add to the tags index
foreach (var tag in _cacheItem.Tags) {
_tagCache.Tag(tag, _cacheKey);
// Don't cache non-200 responses or results of a redirect.
var response = filterContext.HttpContext.Response;
if (response.StatusCode != (int)HttpStatusCode.OK || _transformRedirect) {
return false;
}
// Don't cache in individual route configuration says no.
if (configuration != null && configuration.Duration == 0) {
Logger.Debug("Response for item '{0}' will not be cached because route is configured to not be cached.", _cacheKey);
return false;
}
// Don't cache if request created notifications.
var hasNotifications = !String.IsNullOrEmpty(Convert.ToString(filterContext.Controller.TempData["messages"]));
if (hasNotifications) {
Logger.Debug("Response for item '{0}' will not be cached because one or more notifications were created.", _cacheKey);
return false;
}
return true;
}
private string CaptureResponse(ControllerContext filterContext) {
filterContext.HttpContext.Response.Output = _originalWriter;
protected virtual IDictionary<string, object> GetCacheKeyParameters(ActionExecutingContext filterContext) {
var result = new Dictionary<string, object>();
// Vary by action parameters.
foreach (var p in filterContext.ActionParameters)
result.Add(p.Key, p.Value);
string capturedText = _cachingWriter.ToString();
_cachingWriter.Dispose();
// Vary by theme.
result.Add("theme", _themeManager.GetRequestTheme(filterContext.RequestContext).Id.ToLowerInvariant());
filterContext.HttpContext.Response.Write(capturedText);
return capturedText;
// Vary by configured query string parameters.
var queryString = filterContext.RequestContext.HttpContext.Request.QueryString;
foreach (var key in queryString.AllKeys) {
if (key == null || (CacheSettings.VaryByQueryStringParameters != null && !CacheSettings.VaryByQueryStringParameters.Contains(key)))
continue;
result[key] = queryString[key];
}
// Vary by configured request headers.
var requestHeaders = filterContext.RequestContext.HttpContext.Request.Headers;
foreach (var varyByRequestHeader in CacheSettings.VaryByRequestHeaders) {
if (requestHeaders.AllKeys.Contains(varyByRequestHeader))
result["HEADER:" + varyByRequestHeader] = requestHeaders[varyByRequestHeader];
}
// Vary by request culture if configured.
if (CacheSettings.VaryByCulture) {
result["culture"] = _workContext.CurrentCulture.ToLowerInvariant();
}
// Vary by authentication state if configured.
if (CacheSettings.VaryByAuthenticationState) {
result["auth"] = filterContext.HttpContext.User.Identity.IsAuthenticated.ToString().ToLowerInvariant();
}
return result;
}
private bool TransformRedirect(ActionExecutedContext filterContext) {
protected virtual bool TransformRedirect(ActionExecutedContext filterContext) {
// removes the target of the redirection from cache after a POST
// Removes the target of the redirection from cache after a POST.
if (filterContext.Result == null) {
throw new ArgumentNullException();
@@ -416,48 +378,39 @@ namespace Orchard.OutputCache.Filters {
var redirectResult = filterContext.Result as RedirectResult;
// status code can't be tested at this point, so test the result type instead
if (redirectResult == null ||
!filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)) {
if (redirectResult == null || !filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)) {
return false;
}
Logger.Debug("Redirect on POST");
Logger.Debug("Redirect on POST detected; removing from cache and adding refresh key.");
var redirectUrl = redirectResult.Url;
if (filterContext.HttpContext.Request.IsLocalUrl(redirectUrl)) {
// Remove all cached versions of the same item.
var helper = new UrlHelper(filterContext.HttpContext.Request.RequestContext);
var absolutePath = new Uri(helper.MakeAbsolute(redirectUrl)).AbsolutePath;
// querystring invariant key
var invariantCacheKey = ComputeCacheKey(
_shellSettings.Name,
absolutePath,
() => _workContext.CurrentCulture,
_themeManager.GetRequestTheme(filterContext.RequestContext).Id,
null
);
// remove all cached version of the same page
var invariantCacheKey = ComputeCacheKey(_shellSettings.Name, absolutePath, null);
_cacheService.RemoveByTag(invariantCacheKey);
}
// adding a refresh key so that the redirection doesn't get restored
// from a cached version on a proxy
// this can happen when using public caching, we want to force the
// client to get a fresh copy of the redirectUrl page
// Adding a refresh key so that the redirection doesn't get restored
// from a cached version on a proxy. This can happen when using public
// caching, we want to force the client to get a fresh copy of the
// redirectUrl content.
if (GetMaxAge() > 0) {
if (CacheSettings.DefaultMaxAge > 0) {
var epIndex = redirectUrl.IndexOf('?');
var qs = new NameValueCollection();
if (epIndex > 0) {
qs = HttpUtility.ParseQueryString(redirectUrl.Substring(epIndex));
}
// substract Epoch to get a smaller number
var refresh = _now.Ticks - Epoch;
qs.Remove(RefreshKey);
// Substract Epoch to get a smaller number.
var refresh = _now.Ticks - _epoch;
qs.Remove(_refreshKey);
qs.Add(RefreshKey, refresh.ToString("x"));
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) {
@@ -474,71 +427,131 @@ namespace Orchard.OutputCache.Filters {
return true;
}
public void OnResultExecuting(ResultExecutingContext filterContext) {
private CacheSettings CacheSettings {
get {
return _cacheSettings ?? (_cacheSettings = _cacheManager.Get(CacheSettings.CacheKey, context => {
context.Monitor(_signals.When(CacheSettings.CacheKey));
return new CacheSettings(_workContext.CurrentSite.As<CacheSettingsPart>());
}));
}
}
/// <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;
private void ServeCachedItem(ActionExecutingContext filterContext, CacheItem cacheItem) {
var response = filterContext.HttpContext.Response;
var output = cacheItem.Output;
// Adds some caching information to the output if requested.
if (CacheSettings.DebugMode) {
response.AddHeader("X-Cached-On", cacheItem.CachedOnUtc.ToString("r"));
response.AddHeader("X-Cached-Until", cacheItem.ValidUntilUtc.ToString("r"));
}
// Shorcut action execution.
filterContext.Result = new ContentResult {
Content = output,
ContentType = cacheItem.ContentType
};
response.StatusCode = cacheItem.StatusCode;
ApplyCacheControl(response);
}
private void BeginRenderItem(ActionExecutingContext filterContext) {
var response = filterContext.HttpContext.Response;
ApplyCacheControl(response);
// Intercept the rendered response output.
var originalWriter = response.Output;
var cachingWriter = new StringWriterWithEncoding(originalWriter.Encoding, originalWriter.FormatProvider);
_completeResponseFunc = (ctx) => {
ctx.HttpContext.Response.Output = originalWriter;
string capturedText = cachingWriter.ToString();
cachingWriter.Dispose();
ctx.HttpContext.Response.Write(capturedText);
return capturedText;
};
response.Output = cachingWriter;
}
private void ApplyCacheControl(HttpResponseBase response) {
if (CacheSettings.DefaultMaxAge > 0) {
var maxAge = TimeSpan.FromSeconds(CacheSettings.DefaultMaxAge); //cacheItem.ValidUntilUtc - _clock.UtcNow;
if (maxAge.TotalMilliseconds < 0) {
maxAge = TimeSpan.FromSeconds(0);
maxAge = TimeSpan.Zero;
}
response.Cache.SetCacheability(HttpCacheability.Public);
response.Cache.SetMaxAge(maxAge);
}
response.DisableUserCache();
// keeping this examples for later usage
// Keeping this example for later usage.
// response.DisableKernelCache();
// response.Cache.SetOmitVaryStar(true);
// 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
// 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.
if (HttpRuntime.UsingIntegratedPipeline) {
if (response.Headers.Get("ETag") == null) {
response.Cache.SetETag(cacheItem.GetHashCode().ToString(CultureInfo.InvariantCulture));
// What is the point of GetHashCode() of a newly generated item? /DanielStolt
response.Cache.SetETag(new CacheItem().GetHashCode().ToString(CultureInfo.InvariantCulture));
}
}
if (_varyQueryStringParameters == null) {
if (CacheSettings.VaryByQueryStringParameters == null) {
response.Cache.VaryByParams["*"] = true;
}
else {
foreach (var queryStringParam in _varyQueryStringParameters) {
foreach (var queryStringParam in CacheSettings.VaryByQueryStringParameters) {
response.Cache.VaryByParams[queryStringParam] = true;
}
}
foreach (var varyRequestHeader in _varyRequestHeaders) {
foreach (var varyRequestHeader in CacheSettings.VaryByRequestHeaders) {
response.Cache.VaryByHeaders[varyRequestHeader] = true;
}
}
private string ComputeCacheKey(ControllerContext controllerContext, IEnumerable<KeyValuePair<string, object>> parameters) {
var url = controllerContext.HttpContext.Request.Url.AbsolutePath;
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(";");
protected virtual bool IsIgnoredUrl(string url, IEnumerable<string> ignoredUrls) {
if (ignoredUrls == null || !ignoredUrls.Any()) {
return false;
}
// include the theme in the cache key
keyBuilder.Append("theme=").Append(theme.ToLowerInvariant()).Append(";");
url = url.TrimStart(new[] { '~' });
foreach (var ignoredUrl in ignoredUrls) {
var relativePath = ignoredUrl.TrimStart(new[] { '~' }).Trim();
if (String.IsNullOrWhiteSpace(relativePath)) {
continue;
}
// Ignore comments
if (relativePath.StartsWith("#")) {
continue;
}
if (String.Equals(relativePath, url, StringComparison.OrdinalIgnoreCase)) {
return true;
}
}
return false;
}
protected virtual string ComputeCacheKey(ControllerContext controllerContext, IEnumerable<KeyValuePair<string, object>> parameters) {
var url = controllerContext.HttpContext.Request.Url.AbsolutePath;
return ComputeCacheKey(_shellSettings.Name, url, parameters);
}
protected virtual string ComputeCacheKey(string tenant, string absoluteUrl, IEnumerable<KeyValuePair<string, object>> parameters) {
var keyBuilder = new StringBuilder();
keyBuilder.AppendFormat("tenant={0};url={1};", tenant, absoluteUrl.ToLowerInvariant());
if (parameters != null) {
foreach (var pair in parameters) {
@@ -548,58 +561,8 @@ namespace Orchard.OutputCache.Filters {
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;
}
private int GetMaxAge() {
return _cacheManager.Get("CacheSettingsPart.MaxAge",
context => {
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
return _workContext.CurrentSite.As<CacheSettingsPart>().DefaultMaxAge;
}
);
}
}
public class ViewDataContainer : IViewDataContainer {
public ViewDataDictionary ViewData { get; set; }
}

View File

@@ -1,9 +1,9 @@
using System.Globalization;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Handlers;
using Orchard.Core.Common.Models;
using Orchard.OutputCache.Models;
using Orchard.OutputCache.Services;
using Orchard.ContentManagement;
using Orchard.Core.Common.Models;
using Orchard.ContentManagement.Handlers;
namespace Orchard.OutputCache.Handlers {
public class CacheSettingsPartHandler : ContentHandler {
@@ -14,18 +14,21 @@ namespace Orchard.OutputCache.Handlers {
_cacheService = cacheService;
Filters.Add(new ActivatingFilter<CacheSettingsPart>("Site"));
// initializing default cache settings values
OnInitializing<CacheSettingsPart>((context, part) => { part.DefaultCacheDuration = 300; });
// Default cache settings values.
OnInitializing<CacheSettingsPart>((context, part) => {
part.DefaultCacheDuration = 300;
part.DefaultCacheGraceTime = 60;
});
// evict modified routable content when updated
// 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
// Remove any item tagged with this content item ID.
_cacheService.RemoveByTag(content.ContentItem.Id.ToString(CultureInfo.InvariantCulture));
// search the cache for containers too
// Search the cache for containers too.
var commonPart = content.As<CommonPart>();
if (commonPart != null) {
if (commonPart.Container != null) {

View File

@@ -8,11 +8,11 @@ namespace Orchard.OutputCache {
table => table
.Column<int>("Id", c => c.PrimaryKey().Identity())
.Column<int>("Duration")
.Column<int>("MaxAge")
.Column<int>("GraceTime")
.Column<string>("RouteKey", c => c.WithLength(255))
);
return 6;
return 7;
}
public int UpdateFrom1() {
@@ -31,7 +31,6 @@ namespace Orchard.OutputCache {
}
public int UpdateFrom3() {
SchemaBuilder.AlterTable("CacheParameterRecord",
table => table
.AddColumn<int>("MaxAge")
@@ -47,5 +46,15 @@ namespace Orchard.OutputCache {
public int UpdateFrom5() {
return 6;
}
public int UpdateFrom6() {
SchemaBuilder.AlterTable("CacheParameterRecord",
table => {
table.DropColumn("MaxAge");
table.AddColumn<int>("GraceTime");
});
return 7;
}
}
}

View File

@@ -3,8 +3,9 @@
namespace Orchard.OutputCache.Models {
[Serializable]
public class CacheItem {
public int ValidFor { get; set; }
public DateTime CachedOnUtc { get; set; }
public int Duration { get; set; }
public int GraceTime { get; set; }
public string Output { get; set; }
public string ContentType { get; set; }
public string QueryString { get; set; }
@@ -15,8 +16,28 @@ namespace Orchard.OutputCache.Models {
public int StatusCode { get; set; }
public string[] Tags { get; set; }
public int ValidFor {
get { return Duration; }
}
public DateTime ValidUntilUtc {
get { return CachedOnUtc.AddSeconds(ValidFor); }
}
public bool IsValid(DateTime utcNow) {
return utcNow < ValidUntilUtc;
}
public int StoredFor {
get { return Duration + GraceTime; }
}
public DateTime StoredUntilUtc {
get { return CachedOnUtc.AddSeconds(StoredFor); }
}
public bool IsInGracePeriod(DateTime utcNow) {
return utcNow > ValidUntilUtc && utcNow < StoredUntilUtc;
}
}
}

View File

@@ -7,31 +7,19 @@ namespace Orchard.OutputCache.Models {
[OrchardFeature("Orchard.OutputCache.Database")]
public class CacheItemRecord {
public virtual int Id { get; set; }
public virtual int ValidFor { get; set; }
public virtual DateTime ValidUntilUtc { get; set; }
public virtual DateTime CachedOnUtc { get; set; }
[StringLengthMax]
public virtual string Output { get; set; }
public virtual int Duration { get; set; }
public virtual int GraceTime { get; set; }
public virtual DateTime ValidUntilUtc { get; set; }
public virtual DateTime StoredUntilUtc { get; set; }
[StringLengthMax] public virtual string Output { get; set; }
public virtual string ContentType { get; set; }
[StringLength(2048)]
public virtual string QueryString { get; set; }
[StringLength(2048)]
public virtual string CacheKey { get; set; }
[StringLength(2048)]
public virtual string InvariantCacheKey { get; set; }
[StringLength(2048)]
public virtual string Url { get; set; }
[StringLength(2048)] public virtual string QueryString { get; set; }
[StringLength(2048)] public virtual string CacheKey { get; set; }
[StringLength(2048)] public virtual string InvariantCacheKey { get; set; }
[StringLength(2048)] public virtual string Url { get; set; }
public virtual string Tenant { get; set; }
public virtual int StatusCode { get; set; }
[StringLengthMax]
public virtual string Tags { get; set; }
[StringLengthMax] public virtual string Tags { get; set; }
}
}

View File

@@ -1,11 +1,8 @@
namespace Orchard.OutputCache.Models
{
public class CacheParameterRecord
{
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; }
public virtual int? Duration { get; set; }
public virtual int? GraceTime { get; set; }
}
}

View File

@@ -1,10 +1,11 @@
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; }
}
namespace Orchard.OutputCache.Models {
public class CacheRouteConfig {
public string RouteKey { get; set; }
public string Url { get; set; }
public int Priority { get; set; }
public int? Duration { get; set; }
public int? GraceTime { get; set; }
public int? MaxAge { get; set; }
public string FeatureName { get; set; }
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.ContentManagement;
namespace Orchard.OutputCache.Models {
public class CacheSettings : ContentPart {
public const string CacheKey = "Orchard_OutputCache_CacheSettings";
public CacheSettings(CacheSettingsPart part) {
DefaultCacheDuration = part.DefaultCacheDuration;
DefaultCacheGraceTime = part.DefaultCacheGraceTime;
DefaultMaxAge = part.DefaultMaxAge;
VaryByQueryStringParameters = String.IsNullOrWhiteSpace(part.VaryByQueryStringParameters) ? null : part.VaryByQueryStringParameters.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
VaryByRequestHeaders = String.IsNullOrWhiteSpace(part.VaryByRequestHeaders) ? new HashSet<string>() : new HashSet<string>(part.VaryByRequestHeaders.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray());
VaryByRequestHeaders.Add("HOST"); // Always vary by host name/tenant.
IgnoredUrls = String.IsNullOrWhiteSpace(part.IgnoredUrls) ? null : part.IgnoredUrls.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
IgnoreNoCache = part.IgnoreNoCache;
VaryByCulture = part.VaryByCulture;
CacheAuthenticatedRequests = part.CacheAuthenticatedRequests;
VaryByAuthenticationState = part.VaryByAuthenticationState;
DebugMode = part.DebugMode;
}
public int DefaultCacheDuration { get; private set; }
public int DefaultCacheGraceTime { get; private set; }
public int DefaultMaxAge { get; private set; }
public IEnumerable<string> VaryByQueryStringParameters { get; private set; }
public ISet<string> VaryByRequestHeaders { get; private set; }
public IEnumerable<string> IgnoredUrls { get; private set; }
public bool IgnoreNoCache { get; private set; }
public bool VaryByCulture { get; private set; }
public bool CacheAuthenticatedRequests { get; private set; }
public bool VaryByAuthenticationState { get; private set; }
public bool DebugMode { get; private set; }
}
}

View File

@@ -1,27 +1,47 @@
using System;
using Orchard.ContentManagement;
namespace Orchard.OutputCache.Models {
public class CacheSettingsPart : ContentPart {
public const string CacheKey = "CacheSettingsPart";
public int DefaultCacheDuration {
get { return this.Retrieve(x => x.DefaultCacheDuration, 300); }
set { this.Store(x => x.DefaultCacheDuration, value); }
}
public int DefaultCacheGraceTime {
get { return this.Retrieve(x => x.DefaultCacheGraceTime, 60); }
set { this.Store(x => x.DefaultCacheGraceTime, value); }
}
public int DefaultMaxAge {
get { return this.Retrieve(x => x.DefaultMaxAge); }
set { this.Store(x => x.DefaultMaxAge, value); }
}
public string VaryQueryStringParameters {
get { return this.Retrieve(x => x.VaryQueryStringParameters); }
set { this.Store(x => x.VaryQueryStringParameters, value); }
public string VaryByQueryStringParameters {
get {
return this.Retrieve(
x => x.VaryByQueryStringParameters,
this.Retrieve<string>("VaryQueryStringParameters") // Migrate from old property name.
);
}
set {
this.Store(x => x.VaryByQueryStringParameters, value);
this.Store<string>("VaryQueryStringParameters", null); // Get rid of old property name.
}
}
public string VaryRequestHeaders {
get { return this.Retrieve(x => x.VaryRequestHeaders); }
set { this.Store(x => x.VaryRequestHeaders, value); }
public string VaryByRequestHeaders {
get {
return this.Retrieve(
x => x.VaryByRequestHeaders,
this.Retrieve<string>("VaryRequestHeaders") // Migrate from old property name.
);
}
set {
this.Store(x => x.VaryByRequestHeaders, value);
this.Store<string>("VaryRequestHeaders", null); // Get rid of old property name.
}
}
public string IgnoredUrls {
@@ -29,24 +49,37 @@ namespace Orchard.OutputCache.Models {
set { this.Store(x => x.IgnoredUrls, value); }
}
public bool ApplyCulture {
get { return this.Retrieve(x => x.ApplyCulture); }
set { this.Store(x => x.ApplyCulture, value); }
}
public bool DebugMode {
get { return this.Retrieve(x => x.DebugMode); }
set { this.Store(x => x.DebugMode, value); }
}
public bool IgnoreNoCache {
get { return this.Retrieve(x => x.IgnoreNoCache); }
set { this.Store(x => x.IgnoreNoCache, value); }
}
public bool VaryByCulture {
get {
return this.Retrieve(
x => x.VaryByCulture,
this.Retrieve<bool>("ApplyCulture") // Migrate from old property name.
);
}
set {
this.Store(x => x.VaryByCulture, value);
this.Store<string>("ApplyCulture", null); // Get rid of old property name.
}
}
public bool CacheAuthenticatedRequests {
get { return this.Retrieve(x => x.CacheAuthenticatedRequests); }
set { this.Store(x => x.CacheAuthenticatedRequests, value); }
}
public bool VaryByAuthenticationState {
get { return this.Retrieve(x => x.VaryByAuthenticationState); }
set { this.Store(x => x.VaryByAuthenticationState, value); }
}
public bool DebugMode {
get { return this.Retrieve(x => x.DebugMode); }
set { this.Store(x => x.DebugMode, value); }
}
}
}

View File

@@ -1,15 +1,15 @@
Name: Output Cache
AntiForgery: enabled
Author: Sébastien Ros
Author: The Orchard Team
Website: http://orchardproject.net/
Version: 1.8.1
OrchardVersion: 1.8
Description: Adds Output Caching functionality.
Description: Adds output caching functiona
Features:
Orchard.OutputCache:
Description: Adds Output Caching functionality.
Description: Adds output caching functionality.
Category: Performance
Orchard.OutputCache.Database:
Description: Stores output cache data in the database.
Description: Activates a provider that stores output cache data in the database.
Category: Performance
Dependencies: Orchard.OutputCache

View File

@@ -115,9 +115,10 @@
<Compile Include="Services\DefaultCacheStorageProvider.cs" />
<Compile Include="Services\ICacheService.cs" />
<Compile Include="Services\IOutputCacheStorageProvider.cs" />
<Compile Include="Models\CacheSettings.cs" />
<Compile Include="ViewModels\StatisticsViewModel.cs" />
<Compile Include="ViewModels\IndexViewModel.cs" />
<Compile Include="ViewModels\RouteConfiguration.cs" />
<Compile Include="Models\CacheRouteConfig.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Statistics\Index.cshtml" />

View File

@@ -14,6 +14,8 @@ using Orchard.Utility.Extensions;
namespace Orchard.OutputCache.Services {
public class CacheService : ICacheService {
private const string RouteConfigsCacheKey = "OutputCache_RouteConfigs";
private readonly IWorkContextAccessor _workContextAccessor;
private readonly IRepository<CacheParameterRecord> _repository;
private readonly ICacheManager _cacheManager;
@@ -96,36 +98,36 @@ namespace Orchard.OutputCache.Services {
return _repository.Get(c => c.RouteKey == key);
}
public IEnumerable<RouteConfiguration> GetRouteConfigurations() {
return _cacheManager.Get("GetRouteConfigurations",
public IEnumerable<CacheRouteConfig> GetRouteConfigs() {
return _cacheManager.Get(RouteConfigsCacheKey,
ctx => {
ctx.Monitor(_signals.When("GetRouteConfigurations"));
return _repository.Fetch(c => true).Select(c => new RouteConfiguration { RouteKey = c.RouteKey, Duration = c.Duration }).ToReadOnlyCollection();
ctx.Monitor(_signals.When(RouteConfigsCacheKey));
return _repository.Fetch(c => true).Select(c => new CacheRouteConfig { RouteKey = c.RouteKey, Duration = c.Duration, GraceTime = c.GraceTime }).ToReadOnlyCollection();
});
}
public void SaveCacheConfigurations(IEnumerable<RouteConfiguration> routeConfigurations) {
public void SaveRouteConfigs(IEnumerable<CacheRouteConfig> 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) {
if (!configuration.Duration.HasValue && !configuration.GraceTime.HasValue) {
continue;
}
_repository.Create(new CacheParameterRecord {
Duration = configuration.Duration.Value,
Duration = configuration.Duration,
GraceTime = configuration.GraceTime,
RouteKey = configuration.RouteKey
});
}
// invalidate the cache
_signals.Trigger("GetRouteConfigurations");
_signals.Trigger(RouteConfigsCacheKey);
}
}
}

View File

@@ -42,6 +42,10 @@ namespace Orchard.OutputCache.Services {
private void Convert(CacheItem cacheItem, CacheItemRecord record) {
record.CacheKey = cacheItem.CacheKey;
record.CachedOnUtc = cacheItem.CachedOnUtc;
record.Duration = cacheItem.Duration;
record.GraceTime = cacheItem.GraceTime;
record.ValidUntilUtc = cacheItem.ValidUntilUtc;
record.StoredUntilUtc = cacheItem.StoredUntilUtc;
record.ContentType = cacheItem.ContentType;
record.InvariantCacheKey = cacheItem.InvariantCacheKey;
record.Output = cacheItem.Output;
@@ -50,8 +54,6 @@ namespace Orchard.OutputCache.Services {
record.Tags = String.Join(";", cacheItem.Tags);
record.Tenant = cacheItem.Tenant;
record.Url = cacheItem.Url;
record.ValidFor = cacheItem.ValidFor;
record.ValidUntilUtc = cacheItem.ValidUntilUtc;
}
private CacheItem Convert(CacheItemRecord record) {
@@ -59,6 +61,8 @@ namespace Orchard.OutputCache.Services {
cacheItem.CacheKey = record.CacheKey;
cacheItem.CachedOnUtc = record.CachedOnUtc;
cacheItem.Duration = record.Duration;
cacheItem.GraceTime = record.GraceTime;
cacheItem.ContentType = record.ContentType;
cacheItem.InvariantCacheKey = record.InvariantCacheKey;
cacheItem.Output = record.Output;
@@ -67,7 +71,6 @@ namespace Orchard.OutputCache.Services {
cacheItem.Tags = record.Tags.Split(';');
cacheItem.Tenant = record.Tenant;
cacheItem.Url = record.Url;
cacheItem.ValidFor = record.ValidFor;
return cacheItem;
}
@@ -117,7 +120,7 @@ namespace Orchard.OutputCache.Services {
}
public void RemoveExpiredEntries() {
foreach (var record in _repository.Table.Where( x => x.ValidUntilUtc < _clock.UtcNow)) {
foreach (var record in _repository.Table.Where( x => x.StoredUntilUtc < _clock.UtcNow)) {
_repository.Delete(record);
}
}

View File

@@ -20,7 +20,7 @@ namespace Orchard.OutputCache.Services {
key,
cacheItem,
null,
cacheItem.ValidUntilUtc,
cacheItem.StoredUntilUtc,
System.Web.Caching.Cache.NoSlidingExpiration,
System.Web.Caching.CacheItemPriority.Normal,
null);

View File

@@ -21,19 +21,19 @@ namespace Orchard.OutputCache.Services {
void RemoveByTag(string tag);
/// <summary>
/// Returns the key representing a specific route in the db
/// 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
/// Saves a set of <see cref="CacheRouteConfig"/> to the database.
/// </summary>
/// <param name="routeConfigurations"></param>
void SaveCacheConfigurations(IEnumerable<RouteConfiguration> routeConfigurations);
/// <param name="routeConfigs"></param>
void SaveRouteConfigs(IEnumerable<CacheRouteConfig> routeConfigs);
/// <summary>
/// Returns all defined configurations for specific routes
/// Returns all defined configurations for specific routes.
/// </summary>
IEnumerable<RouteConfiguration> GetRouteConfigurations();
IEnumerable<CacheRouteConfig> GetRouteConfigs();
}
}

View File

@@ -1,19 +1,21 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Orchard.OutputCache.Models;
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 List<CacheRouteConfig> RouteConfigs { get; set; }
[Range(0, Int32.MaxValue), Required] public int DefaultCacheDuration { get; set; }
[Range(0, Int32.MaxValue), Required] public int DefaultCacheGraceTime { get; set; }
[Range(0, Int32.MaxValue), Required] public int DefaultMaxAge { get; set; }
public string VaryByQueryStringParameters { get; set; }
public string VaryByRequestHeaders { get; set; }
public string IgnoredUrls { get; set; }
public bool ApplyCulture { get; set; }
public bool DebugMode { get; set; }
public string VaryQueryStringParameters { get; set; }
public string VaryRequestHeaders { get; set; }
public bool IgnoreNoCache { get; set; }
public bool VaryByCulture { get; set; }
public bool CacheAuthenticatedRequests { get; set; }
public bool VaryByAuthenticationState { get; set; }
public bool DebugMode { get; set; }
}
}

View File

@@ -3,10 +3,11 @@
@{
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));
// Group configurations by feature name.
var featureRouteConfigs =
Model.RouteConfigs
.GroupBy(x => x.FeatureName)
.ToDictionary(x => x.Key, x => x.Select(y => y));
}
@using (Html.BeginFormAntiForgeryPost())
@@ -16,9 +17,15 @@
<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>
<span class="hint">@T("Number of seconds that items should be cached on the server before being regenerated.")</span>
</fieldset>
<fieldset>
<label>@T("Default Cache Grace Time")</label>
@Html.TextBoxFor(m => m.DefaultCacheGraceTime, new { @class = "text small" })
<span class="hint">@T("Number of seconds past duration that stale items can be served from cache while regenaration is in progress. Enter 0 to disable grace time by default.")</span>
</fieldset>
<fieldset>
<label>@T("Max Age")</label>
@Html.TextBoxFor(m => m.DefaultMaxAge, new { @class = "text small" })
@@ -26,72 +33,78 @@
</fieldset>
<fieldset>
<label>@T("Headers")</label>
@Html.CheckBoxFor(m => m.IgnoreNoCache) <label for="@Html.FieldIdFor(m => m.IgnoreNoCache)" class="forcheckbox">@T("Ignore no-cache headers")</label>
<span class="hint">@T("When checked, any request containing a 'Content-Cache: no-cache' header will still return cached values if available.")</span>
</fieldset>
<fieldset>
<label>@T("Vary Query String Parameters")</label>
@Html.TextBoxFor(m => m.VaryQueryStringParameters, new { @class = "text medium" })
<span class="hint">@T("When defined, using comma separated values, sets caching to vary via specified query string parameters")</span>
<label>@T("Vary by Query String Parameters")</label>
@Html.TextBoxFor(m => m.VaryByQueryStringParameters, new { @class = "text medium" })
<span class="hint">@T("When defined, using comma separated values, sets caching to vary by the specified query string parameters. Leave empty to vary by all query string parameters.")</span>
</fieldset>
<fieldset>
<label>@T("Vary Request Headers")</label>
@Html.TextBoxFor(m => m.VaryRequestHeaders, new { @class = "text medium" })
<span class="hint">@T("When defined, using comma separated values, sets caching to vary via specified request headers.")</span>
<label>@T("Vary by Request Headers")</label>
@Html.TextBoxFor(m => m.VaryByRequestHeaders, new { @class = "text medium" })
<span class="hint">@T("When defined, using comma separated values, sets caching to vary by the specified request headers.")</span>
</fieldset>
<fieldset>
<label>@T("Ignored urls")</label>
<label>@T("Ignored URLs")</label>
@Html.TextAreaFor(m => m.IgnoredUrls, new { @class = "text medium" })
<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>
<span class="hint">@T("Specifies a set of paths relative to tenant root (e.g. '/' or '/About') for which caching should not be done. Enter one path per line.")</span>
<span class="hint">@T("You can add comments by starting a 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 better performance, leave this unchecked if your website uses only one culture.")</span>
</fieldset>
<label>@T("Advanced Settings")</label>
@Html.CheckBoxFor(m => m.IgnoreNoCache)
<label for="@Html.FieldIdFor(m => m.IgnoreNoCache)" class="forcheckbox">@T("Ignore no-cache headers")</label>
@Html.Hint(T("When checked, requests containing a 'Content-Cache: no-cache' header will still return cached values if available."))
@Html.CheckBoxFor(m => m.VaryByCulture)
<label for="@Html.FieldIdFor(m => m.VaryByCulture)" class="forcheckbox">@T("Vary by culture")</label>
@Html.Hint(T("When checked, caching will vary by request culture. For better performance, leave this unchecked if your site uses only one culture."))
<fieldset>
<label>@T("Authenticated Requests")</label>
@Html.CheckBoxFor(m => m.CacheAuthenticatedRequests)
<label for="@Html.FieldIdFor(m => m.CacheAuthenticatedRequests)" class="forcheckbox">@T("Cache Authenticated Requests")</label>
@Html.Hint(T("When checked, the output cache filter will store the HTML output to cache and retrieve from cache even if the user is authenticated. When unchecked, it will bypass cache for authenticated users."))
<label for="@Html.FieldIdFor(m => m.CacheAuthenticatedRequests)" class="forcheckbox">@T("Cache authenticated requests")</label>
@Html.Hint(T("When checked, caching will apply even if the user is authenticated. When unchecked, caching will be bypassed for authenticated requests."))
@Html.CheckBoxFor(m => m.VaryByAuthenticationState)
<label for="@Html.FieldIdFor(m => m.VaryByAuthenticationState)" class="forcheckbox">@T("Vary by authentication state")</label>
@Html.Hint(T("When checked (and 'Cache authenticated requests' is also checked) caching will vary by whether the user is authenticated or not. This is useful if content is rendered differently for authenticated vs. anonymous users."))
@Html.CheckBoxFor(m => m.DebugMode)
<label for="@Html.FieldIdFor(m => m.DebugMode)" class="forcheckbox">@T("Render caching information in cached pages")</label>
</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) {
foreach (var feature in featureRouteConfigs.Keys) {
<h2>@T(feature)</h2>
<table class="items">
<thead>
<tr>
<th scope="col">@T("Route")</th>
<th scope="col" style="width: 100%;">@T("Route")</th>
<th scope="col">@T("Priority")</th>
<th scope="col">@T("Duration")</th>
<th scope="col">@T("Duration (*)")</th>
<th scope="col">@T("Grace Time (**)")</th>
</tr>
</thead>
@foreach (var routeConfiguration in featureRouteConfigurations[feature]) {
var index = Model.RouteConfigurations.IndexOf(routeConfiguration);
@foreach (var routeConfig in featureRouteConfigs[feature]) {
var index = Model.RouteConfigs.IndexOf(routeConfig);
<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 style="width: 100%;">@routeConfig.Url</td>
<td>@routeConfig.Priority</td>
<td>
@Html.TextBoxFor(m => m.RouteConfigs[index].Duration, new { @class = "text small" })
</td>
@Html.HiddenFor(m => m.RouteConfigurations[index].RouteKey)
<td>
@Html.TextBoxFor(m => m.RouteConfigs[index].GraceTime, new { @class = "text small" })
</td>
@Html.HiddenFor(m => m.RouteConfigs[index].RouteKey)
</tr>
}
</table>
}
<span class="hint">@T("* Leave Duration column empty to use default duration, enter 0 to disable caching for the route.")</span>
<span class="hint">@T("** Leave Grace Time column empty to use default grace time, enter 0 to disable grace time for the route.")</span>
<button class="primaryAction" type="submit">@T("Save")</button>
<button class="primaryAction" type="submit" style="margin-top: 1em;">@T("Save")</button>
}

View File

@@ -1,8 +1,10 @@
@using Orchard.OutputCache.ViewModels
@using Orchard.Localization.Services
@using Orchard.OutputCache.ViewModels
@model StatisticsViewModel
@{
Layout.Title = T("Cache Statistics");
var dateTimeFormatProvider = WorkContext.Resolve<IDateTimeFormatProvider>();
}
@using (Html.BeginFormAntiForgeryPost()) {
@@ -10,14 +12,15 @@
<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>
<h2>@T("Cached Items")</h2>
<table class="items">
<thead>
<tr>
<th scope="col">@T("Url")</th>
<th scope="col">@T("URL")</th>
<th scope="col">@T("Cache Key")</th>
<th scope="col">@T("Cached On")</th>
<th scope="col">@T("Cached Until")</th>
<th scope="col">@T("Cached")</th>
<th scope="col">@T("Valid Until")</th>
<th scope="col">@T("Stored Until")</th>
<th scope="col">&nbsp;</th>
</tr>
</thead>
@@ -26,7 +29,8 @@
<td><span title="@cacheItem.QueryString">@cacheItem.Url</span></td>
<td>@cacheItem.CacheKey</td>
<td>@Display.DateTimeRelative(DateTimeUtc: cacheItem.CachedOnUtc)</td>
<td>@cacheItem.ValidUntilUtc.ToLocalTime()</td>
<td>@Display.DateTime(DateTimeUtc: cacheItem.ValidUntilUtc, CustomFormat: T("{0} {1}", dateTimeFormatProvider.ShortDateFormat, dateTimeFormatProvider.LongTimeFormat))</td>
<td>@Display.DateTime(DateTimeUtc: cacheItem.StoredUntilUtc, CustomFormat: T("{0} {1}", dateTimeFormatProvider.ShortDateFormat, dateTimeFormatProvider.LongTimeFormat))</td>
<td>@Html.ActionLink(T("Evict").Text, "Evict", new { Area = "Orchard.OutputCache", Controller = "Statistics", cacheKey = cacheItem.CacheKey })</td>
</tr>
}