mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
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:
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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"> </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>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user