mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-15 19:54:57 +08:00
Importing the Contrib.Cache module
--HG-- branch : 1.x
This commit is contained in:
19
src/Orchard.Web/Modules/Orchard.OutputCache/AdminMenu.cs
Normal file
19
src/Orchard.Web/Modules/Orchard.OutputCache/AdminMenu.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Orchard.Localization;
|
||||
using Orchard.Security;
|
||||
using Orchard.UI.Navigation;
|
||||
|
||||
namespace Orchard.OutputCache {
|
||||
public class AdminMenu : INavigationProvider {
|
||||
public Localizer T { get; set; }
|
||||
public string MenuName { get { return "admin"; } }
|
||||
|
||||
public void GetNavigation(NavigationBuilder builder) {
|
||||
builder
|
||||
.Add(T("Settings"), menu => menu
|
||||
.Add(T("Cache"), "10.0", subMenu => subMenu.Action("Index", "Admin", new { area = "Orchard.OutputCache" }).Permission(StandardPermissions.SiteOwner)
|
||||
.Add(T("Settings"), "10.0", item => item.Action("Index", "Admin", new { area = "Orchard.OutputCache" }).Permission(StandardPermissions.SiteOwner).LocalNav())
|
||||
.Add(T("Statistics"), "10.0", item => item.Action("Index", "Statistics", new { area = "Orchard.OutputCache" }).Permission(StandardPermissions.SiteOwner).LocalNav())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,130 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Web.Mvc;
|
||||
using System.Web.Routing;
|
||||
using Autofac.Features.Metadata;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.OutputCache.Services;
|
||||
using Orchard.OutputCache.ViewModels;
|
||||
using Orchard;
|
||||
using Orchard.Caching;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Mvc.Routes;
|
||||
using Orchard.Security;
|
||||
using Orchard.UI.Admin;
|
||||
using Orchard.UI.Notify;
|
||||
|
||||
namespace Orchard.OutputCache.Controllers {
|
||||
[Admin]
|
||||
public class AdminController : Controller {
|
||||
private readonly IEnumerable<Meta<IRouteProvider>> _routeProviders;
|
||||
private readonly ISignals _signals;
|
||||
private readonly ICacheService _cacheService;
|
||||
|
||||
public AdminController(
|
||||
IOrchardServices services,
|
||||
IEnumerable<Meta<IRouteProvider>> routeProviders,
|
||||
ISignals signals,
|
||||
ICacheService cacheService) {
|
||||
_routeProviders = routeProviders;
|
||||
_signals = signals;
|
||||
_cacheService = cacheService;
|
||||
Services = services;
|
||||
}
|
||||
|
||||
public IOrchardServices Services { get; set; }
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public ActionResult Index() {
|
||||
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
var routeConfigurations = new List<RouteConfiguration>();
|
||||
var settings = Services.WorkContext.CurrentSite.As<CacheSettingsPart>();
|
||||
|
||||
|
||||
foreach (var routeProvider in _routeProviders) {
|
||||
// right now, ignore generic routes
|
||||
if (routeProvider.Value is StandardExtensionRouteProvider) continue;
|
||||
|
||||
var routeCollection = routeProvider.Value.GetRoutes();
|
||||
var feature = routeProvider.Metadata["Feature"] as Orchard.Environment.Extensions.Models.Feature;
|
||||
|
||||
// if there is no feature, skip route
|
||||
if (feature == null) continue;
|
||||
|
||||
foreach (var routeDescriptor in routeCollection) {
|
||||
var route = routeDescriptor.Route as Route;
|
||||
|
||||
if(route == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ignore admin routes
|
||||
if (route.Url.StartsWith("Admin/") || route.Url == "Admin") continue;
|
||||
|
||||
var cacheParameterKey = _cacheService.GetRouteDescriptorKey(HttpContext, route);
|
||||
var cacheParameter = _cacheService.GetCacheParameterByKey(cacheParameterKey);
|
||||
var duration = cacheParameter == null ? default(int?) : cacheParameter.Duration;
|
||||
|
||||
routeConfigurations.Add(new RouteConfiguration {
|
||||
RouteKey = cacheParameterKey,
|
||||
Url = route.Url,
|
||||
Priority = routeDescriptor.Priority,
|
||||
Duration = duration,
|
||||
FeatureName =
|
||||
String.IsNullOrWhiteSpace(feature.Descriptor.Name)
|
||||
? feature.Descriptor.Id
|
||||
: feature.Descriptor.Name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var model = new IndexViewModel {
|
||||
DefaultCacheDuration = settings.DefaultCacheDuration,
|
||||
DefaultMaxAge = settings.DefaultMaxAge,
|
||||
VaryQueryStringParameters = settings.VaryQueryStringParameters,
|
||||
IgnoredUrls = settings.IgnoredUrls,
|
||||
DebugMode = settings.DebugMode,
|
||||
ApplyCulture = settings.ApplyCulture,
|
||||
RouteConfigurations = routeConfigurations
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost, ActionName("Index")]
|
||||
public ActionResult IndexPost() {
|
||||
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
var model = new IndexViewModel {
|
||||
RouteConfigurations = new List<RouteConfiguration>()
|
||||
};
|
||||
|
||||
if(TryUpdateModel(model)) {
|
||||
var settings = Services.WorkContext.CurrentSite.As<CacheSettingsPart>();
|
||||
settings.DefaultCacheDuration = model.DefaultCacheDuration;
|
||||
settings.DefaultMaxAge = model.DefaultMaxAge;
|
||||
settings.VaryQueryStringParameters = model.VaryQueryStringParameters;
|
||||
settings.IgnoredUrls = model.IgnoredUrls;
|
||||
settings.DebugMode = model.DebugMode;
|
||||
settings.ApplyCulture = model.ApplyCulture;
|
||||
|
||||
// invalidates the settings cache
|
||||
_signals.Trigger(CacheSettingsPart.CacheKey);
|
||||
|
||||
_cacheService.SaveCacheConfigurations(model.RouteConfigurations);
|
||||
|
||||
Services.Notifier.Information(T("Cache Settings saved successfully."));
|
||||
}
|
||||
else {
|
||||
Services.Notifier.Error(T("Could not save Cache Settings."));
|
||||
}
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.OutputCache.Services;
|
||||
using Orchard.OutputCache.ViewModels;
|
||||
using Orchard;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Security;
|
||||
using Orchard.Settings;
|
||||
using Orchard.UI.Admin;
|
||||
using Orchard.UI.Navigation;
|
||||
|
||||
namespace Orchard.OutputCache.Controllers {
|
||||
[Admin]
|
||||
public class StatisticsController : Controller {
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly IOutputCacheStorageProvider _cacheStorageProvider;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
private readonly ISiteService _siteService;
|
||||
|
||||
public StatisticsController(
|
||||
IOrchardServices services,
|
||||
ICacheService cacheService,
|
||||
IOutputCacheStorageProvider cacheStorageProvider,
|
||||
ShellSettings shellSettings,
|
||||
ISiteService siteService) {
|
||||
_cacheService = cacheService;
|
||||
_cacheStorageProvider = cacheStorageProvider;
|
||||
_shellSettings = shellSettings;
|
||||
_siteService = siteService;
|
||||
Services = services;
|
||||
}
|
||||
|
||||
public IOrchardServices Services { get; set; }
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public ActionResult Index(PagerParameters pagerParameters) {
|
||||
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
var pager = new Pager(_siteService.GetSiteSettings(), pagerParameters);
|
||||
var pagerShape = Services.New.Pager(pager).TotalItemCount(_cacheStorageProvider.GetCacheItemsCount());
|
||||
|
||||
var model = new StatisticsViewModel {
|
||||
CacheItems = _cacheStorageProvider
|
||||
.GetCacheItems(pager.GetStartIndex(), pager.PageSize)
|
||||
.ToList(),
|
||||
Pager = pagerShape
|
||||
};
|
||||
|
||||
return View(model);
|
||||
}
|
||||
|
||||
public ActionResult Evict(string cacheKey) {
|
||||
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
_cacheStorageProvider.Remove(cacheKey);
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult EvictAll() {
|
||||
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner, T("Not allowed to manage cache")))
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
_cacheStorageProvider.RemoveAll();
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,669 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using System.Web.Mvc;
|
||||
using System.Web.Routing;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.OutputCache.Services;
|
||||
using Orchard;
|
||||
using Orchard.Caching;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Mvc.Filters;
|
||||
using Orchard.Services;
|
||||
using Orchard.Themes;
|
||||
using Orchard.UI.Admin;
|
||||
using Orchard.Utility.Extensions;
|
||||
|
||||
namespace Orchard.OutputCache.Filters
|
||||
{
|
||||
public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter
|
||||
{
|
||||
|
||||
private readonly ICacheManager _cacheManager;
|
||||
private readonly IOutputCacheStorageProvider _cacheStorageProvider;
|
||||
private readonly ITagCache _tagCache;
|
||||
private readonly IDisplayedContentItemHandler _displayedContentItemHandler;
|
||||
private readonly IWorkContextAccessor _workContextAccessor;
|
||||
private readonly IThemeManager _themeManager;
|
||||
private readonly IClock _clock;
|
||||
private readonly ICacheService _cacheService;
|
||||
private readonly ISignals _signals;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
|
||||
private const string AntiforgeryBeacon = "<!--OutputCacheFilterAntiForgeryToken-->";
|
||||
private const string AntiforgeryTag = "<input name=\"__RequestVerificationToken\" type=\"hidden\" value=\"";
|
||||
private const string RefreshKey = "__r";
|
||||
|
||||
public OutputCacheFilter(
|
||||
ICacheManager cacheManager,
|
||||
IOutputCacheStorageProvider cacheStorageProvider,
|
||||
ITagCache tagCache,
|
||||
IDisplayedContentItemHandler displayedContentItemHandler,
|
||||
IWorkContextAccessor workContextAccessor,
|
||||
IThemeManager themeManager,
|
||||
IClock clock,
|
||||
ICacheService cacheService,
|
||||
ISignals signals,
|
||||
ShellSettings shellSettings)
|
||||
{
|
||||
_cacheManager = cacheManager;
|
||||
_cacheStorageProvider = cacheStorageProvider;
|
||||
_tagCache = tagCache;
|
||||
_displayedContentItemHandler = displayedContentItemHandler;
|
||||
_workContextAccessor = workContextAccessor;
|
||||
_themeManager = themeManager;
|
||||
_clock = clock;
|
||||
_cacheService = cacheService;
|
||||
_signals = signals;
|
||||
_shellSettings = shellSettings;
|
||||
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
private bool _debugMode;
|
||||
private int _cacheDuration;
|
||||
private int _maxAge;
|
||||
private string _ignoredUrls;
|
||||
private bool _applyCulture;
|
||||
private string _cacheKey;
|
||||
private string _invariantCacheKey;
|
||||
private string _actionName;
|
||||
private DateTime _now;
|
||||
private string[] _varyQueryStringParameters;
|
||||
|
||||
|
||||
private WorkContext _workContext;
|
||||
private CapturingResponseFilter _filter;
|
||||
private CacheItem _cacheItem;
|
||||
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext filterContext)
|
||||
{
|
||||
// use the action in the cacheKey so that the same route can't return cache for different actions
|
||||
_actionName = filterContext.ActionDescriptor.ActionName;
|
||||
|
||||
// apply OutputCacheAttribute logic if defined
|
||||
var outputCacheAttribute = filterContext.ActionDescriptor.GetCustomAttributes(typeof (OutputCacheAttribute), true).Cast<OutputCacheAttribute>().FirstOrDefault() ;
|
||||
|
||||
if(outputCacheAttribute != null) {
|
||||
if (outputCacheAttribute.Duration <= 0 || outputCacheAttribute.NoStore) {
|
||||
Logger.Debug("Request ignored based on OutputCache attribute");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// saving the current datetime
|
||||
_now = _clock.UtcNow;
|
||||
|
||||
// before executing an action, we check if a valid cached result is already
|
||||
// existing for this context (url, theme, culture, tenant)
|
||||
|
||||
Logger.Debug("Request on: " + filterContext.RequestContext.HttpContext.Request.RawUrl);
|
||||
|
||||
// don't cache POST requests
|
||||
if(filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) ) {
|
||||
Logger.Debug("Request ignored on POST");
|
||||
return;
|
||||
}
|
||||
|
||||
// don't cache the admin
|
||||
if (AdminFilter.IsApplied(new RequestContext(filterContext.HttpContext, new RouteData()))) {
|
||||
Logger.Debug("Request ignored on Admin section");
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore child actions, e.g. HomeController is using RenderAction()
|
||||
if (filterContext.IsChildAction){
|
||||
Logger.Debug("Request ignored on Child actions");
|
||||
return;
|
||||
}
|
||||
|
||||
_workContext = _workContextAccessor.GetContext();
|
||||
|
||||
// don't return any cached content, or cache any content, if the user is authenticated
|
||||
if (_workContext.CurrentUser != null) {
|
||||
Logger.Debug("Request ignored on Authenticated user");
|
||||
return;
|
||||
}
|
||||
|
||||
// caches the default cache duration to prevent a query to the settings
|
||||
_cacheDuration = _cacheManager.Get("CacheSettingsPart.Duration",
|
||||
context => {
|
||||
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
|
||||
return _workContext.CurrentSite.As<CacheSettingsPart>().DefaultCacheDuration;
|
||||
}
|
||||
);
|
||||
|
||||
// caches the default max age duration to prevent a query to the settings
|
||||
_maxAge = _cacheManager.Get("CacheSettingsPart.MaxAge",
|
||||
context => {
|
||||
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
|
||||
return _workContext.CurrentSite.As<CacheSettingsPart>().DefaultMaxAge;
|
||||
}
|
||||
);
|
||||
|
||||
_varyQueryStringParameters = _cacheManager.Get("CacheSettingsPart.VaryQueryStringParameters",
|
||||
context => {
|
||||
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
|
||||
var varyQueryStringParameters = _workContext.CurrentSite.As<CacheSettingsPart>().VaryQueryStringParameters;
|
||||
|
||||
return string.IsNullOrWhiteSpace(varyQueryStringParameters) ? null
|
||||
: varyQueryStringParameters.Split(new[]{","}, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
|
||||
}
|
||||
);
|
||||
|
||||
// caches the ignored urls to prevent a query to the settings
|
||||
_ignoredUrls = _cacheManager.Get("CacheSettingsPart.IgnoredUrls",
|
||||
context => {
|
||||
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
|
||||
return _workContext.CurrentSite.As<CacheSettingsPart>().IgnoredUrls;
|
||||
}
|
||||
);
|
||||
|
||||
// caches the culture setting
|
||||
_applyCulture = _cacheManager.Get("CacheSettingsPart.ApplyCulture",
|
||||
context => {
|
||||
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
|
||||
return _workContext.CurrentSite.As<CacheSettingsPart>().ApplyCulture;
|
||||
}
|
||||
);
|
||||
|
||||
// caches the ignored urls to prevent a query to the settings
|
||||
_debugMode = _cacheManager.Get("CacheSettingsPart.DebugMode",
|
||||
context => {
|
||||
context.Monitor(_signals.When(CacheSettingsPart.CacheKey));
|
||||
return _workContext.CurrentSite.As<CacheSettingsPart>().DebugMode;
|
||||
}
|
||||
);
|
||||
|
||||
var queryString = filterContext.RequestContext.HttpContext.Request.QueryString;
|
||||
var parameters = new Dictionary<string, object>(filterContext.ActionParameters);
|
||||
|
||||
foreach(var key in queryString.AllKeys) {
|
||||
if (key == null) continue;
|
||||
|
||||
parameters[key] = queryString[key];
|
||||
}
|
||||
|
||||
// compute the cache key
|
||||
_cacheKey = ComputeCacheKey(filterContext, parameters);
|
||||
|
||||
// create a tag which doesn't care about querystring
|
||||
_invariantCacheKey = ComputeCacheKey(filterContext, null);
|
||||
|
||||
// don't retrieve cache content if refused
|
||||
// in this case the result of the action will update the current cached version
|
||||
if (filterContext.RequestContext.HttpContext.Request.Headers["Cache-Control"] != "no-cache") {
|
||||
|
||||
// fetch cached data
|
||||
_cacheItem = _cacheStorageProvider.GetCacheItem(_cacheKey);
|
||||
|
||||
if (_cacheItem == null) {
|
||||
Logger.Debug("Cached version not found");
|
||||
}
|
||||
}
|
||||
else {
|
||||
Logger.Debug("Cache-Control = no-cache requested");
|
||||
}
|
||||
|
||||
var response = filterContext.HttpContext.Response;
|
||||
|
||||
// render cached content
|
||||
if (_cacheItem != null)
|
||||
{
|
||||
Logger.Debug("Cache item found, expires on " + _cacheItem.ValidUntilUtc);
|
||||
|
||||
var output = _cacheItem.Output;
|
||||
|
||||
/*
|
||||
*
|
||||
* There is no need to replace the AntiForgeryToken as it is not used for unauthenticated requests
|
||||
* and at this point, the request can't be authenticated
|
||||
*
|
||||
*
|
||||
|
||||
// replace any anti forgery token with a fresh value
|
||||
if (output.Contains(AntiforgeryBeacon))
|
||||
{
|
||||
var viewContext = new ViewContext
|
||||
{
|
||||
HttpContext = filterContext.HttpContext,
|
||||
Controller = filterContext.Controller
|
||||
};
|
||||
|
||||
var htmlHelper = new HtmlHelper(viewContext, new ViewDataContainer());
|
||||
var siteSalt = _workContext.CurrentSite.SiteSalt;
|
||||
var token = htmlHelper.AntiForgeryToken(siteSalt);
|
||||
output = output.Replace(AntiforgeryBeacon, token.ToString());
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
// adds some caching information to the output if requested
|
||||
if (_debugMode)
|
||||
{
|
||||
output += "\r\n<!-- Cached on " + _cacheItem.CachedOnUtc + " (UTC) until " + _cacheItem.ValidUntilUtc + " (UTC) -->";
|
||||
response.AddHeader("X-Cached-On", _cacheItem.CachedOnUtc.ToString("r"));
|
||||
response.AddHeader("X-Cached-Until", _cacheItem.ValidUntilUtc.ToString("r"));
|
||||
}
|
||||
|
||||
filterContext.Result = new ContentResult
|
||||
{
|
||||
Content = output,
|
||||
ContentType = _cacheItem.ContentType
|
||||
};
|
||||
|
||||
response.StatusCode = _cacheItem.StatusCode;
|
||||
|
||||
ApplyCacheControl(_cacheItem, response);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_cacheItem = new CacheItem();
|
||||
|
||||
// get contents
|
||||
ApplyCacheControl(_cacheItem, response);
|
||||
|
||||
// no cache content available, intercept the execution results for caching
|
||||
response.Filter = _filter = new CapturingResponseFilter(response.Filter);
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext filterContext) {
|
||||
|
||||
// ignore error results from cache
|
||||
if (filterContext.HttpContext.Response.StatusCode != (int)HttpStatusCode.OK) {
|
||||
_filter = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// if the result of a POST is a Redirect, remove any Cache Item for this url
|
||||
// so that the redirected client gets a fresh result
|
||||
// also add a random token to the query string so that public cachers (IIS, proxies, ...) don't return cached content
|
||||
// i.e., Comment creation
|
||||
|
||||
// ignore in admin
|
||||
if (AdminFilter.IsApplied(new RequestContext(filterContext.HttpContext, new RouteData()))) {
|
||||
return;
|
||||
}
|
||||
|
||||
_workContext = _workContextAccessor.GetContext();
|
||||
|
||||
// ignore authenticated requests
|
||||
if (_workContext.CurrentUser != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)
|
||||
&& filterContext.Result is RedirectResult) {
|
||||
|
||||
Logger.Debug("Redirect on POST");
|
||||
var redirectUrl = ((RedirectResult)filterContext.Result).Url;
|
||||
|
||||
if (!VirtualPathUtility.IsAbsolute(redirectUrl)) {
|
||||
var applicationRoot = filterContext.HttpContext.Request.ToRootUrlString();
|
||||
if (redirectUrl.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) {
|
||||
redirectUrl = redirectUrl.Substring(applicationRoot.Length);
|
||||
}
|
||||
}
|
||||
|
||||
// querystring invariant key
|
||||
var invariantCacheKey = ComputeCacheKey(
|
||||
_shellSettings.Name,
|
||||
redirectUrl,
|
||||
() => _workContext.CurrentCulture,
|
||||
_themeManager.GetRequestTheme(filterContext.RequestContext).Id,
|
||||
null
|
||||
);
|
||||
|
||||
_cacheService.RemoveByTag(invariantCacheKey);
|
||||
|
||||
// adding a refresh key so that the next request will not be cached
|
||||
var epIndex = redirectUrl.IndexOf('?');
|
||||
var qs = new NameValueCollection();
|
||||
if (epIndex > 0) {
|
||||
qs = HttpUtility.ParseQueryString(redirectUrl.Substring(epIndex));
|
||||
}
|
||||
|
||||
var refresh = _now.Ticks;
|
||||
qs.Remove(RefreshKey);
|
||||
|
||||
qs.Add(RefreshKey, refresh.ToString("x"));
|
||||
var querystring = "?" + string.Join("&", Array.ConvertAll(qs.AllKeys, k => string.Format("{0}={1}", HttpUtility.UrlEncode(k), HttpUtility.UrlEncode(qs[k]))));
|
||||
|
||||
if (epIndex > 0) {
|
||||
redirectUrl = redirectUrl.Substring(0, epIndex) + querystring;
|
||||
}
|
||||
else {
|
||||
redirectUrl = redirectUrl + querystring;
|
||||
}
|
||||
|
||||
filterContext.Result = new RedirectResult(redirectUrl, ((RedirectResult)filterContext.Result).Permanent);
|
||||
filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnResultExecuted(ResultExecutedContext filterContext)
|
||||
{
|
||||
var response = filterContext.HttpContext.Response;
|
||||
|
||||
// save the result only if the content can be intercepted
|
||||
if (_filter == null) return;
|
||||
|
||||
// only for ViewResult right now, as we don't want to handle redirects, HttpNotFound, ...
|
||||
if (filterContext.Result as ViewResultBase == null) {
|
||||
Logger.Debug("Ignoring none ViewResult response");
|
||||
return;
|
||||
}
|
||||
|
||||
// check if there is a specific rule not to cache the whole route
|
||||
var configurations = _cacheService.GetRouteConfigurations();
|
||||
var route = filterContext.Controller.ControllerContext.RouteData.Route;
|
||||
var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
|
||||
var configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
|
||||
|
||||
// do not cache ?
|
||||
if (configuration != null && configuration.Duration == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// ignored url ?
|
||||
if (IsIgnoredUrl(filterContext.RequestContext.HttpContext.Request.AppRelativeCurrentExecutionFilePath, _ignoredUrls))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(response.IsClientConnected)
|
||||
response.Flush();
|
||||
|
||||
var output = _filter.GetContents(response.ContentEncoding);
|
||||
|
||||
if (String.IsNullOrWhiteSpace(output))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var tokenIndex = output.IndexOf(AntiforgeryTag, StringComparison.Ordinal);
|
||||
|
||||
// substitute antiforgery token by a beacon
|
||||
if (tokenIndex != -1)
|
||||
{
|
||||
var tokenEnd = output.IndexOf(">", tokenIndex, StringComparison.Ordinal);
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(output.Substring(0, tokenIndex));
|
||||
sb.Append(AntiforgeryBeacon);
|
||||
sb.Append(output.Substring(tokenEnd + 1));
|
||||
|
||||
output = sb.ToString();
|
||||
}
|
||||
|
||||
// default duration of specific one ?
|
||||
var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : _cacheDuration;
|
||||
|
||||
// include each of the content item ids as tags for the cache entry
|
||||
var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
|
||||
|
||||
_cacheItem.ContentType = response.ContentType;
|
||||
_cacheItem.StatusCode = response.StatusCode;
|
||||
_cacheItem.CachedOnUtc = _now;
|
||||
_cacheItem.ValidFor = cacheDuration;
|
||||
_cacheItem.QueryString = filterContext.HttpContext.Request.Url.Query;
|
||||
_cacheItem.Output = output;
|
||||
_cacheItem.CacheKey = _cacheKey;
|
||||
_cacheItem.InvariantCacheKey = _invariantCacheKey;
|
||||
_cacheItem.Tenant = _shellSettings.Name;
|
||||
_cacheItem.Url = filterContext.HttpContext.Request.Url.AbsolutePath;
|
||||
_cacheItem.Tags = new[] { _invariantCacheKey }.Union(contentItemIds).ToArray();
|
||||
|
||||
Logger.Debug("Cache item added: " + _cacheItem.CacheKey);
|
||||
|
||||
// remove old cache data
|
||||
_cacheService.RemoveByTag(_invariantCacheKey);
|
||||
|
||||
// add data to cache
|
||||
_cacheStorageProvider.Set(_cacheKey, _cacheItem);
|
||||
|
||||
// add to the tags index
|
||||
foreach (var tag in _cacheItem.Tags) {
|
||||
_tagCache.Tag(tag, _cacheKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void OnResultExecuting(ResultExecutingContext filterContext)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Define valid cache control values
|
||||
/// </summary>
|
||||
private void ApplyCacheControl(CacheItem cacheItem, HttpResponseBase response) {
|
||||
if (_maxAge > 0) {
|
||||
var maxAge = new TimeSpan(0, 0, 0, _maxAge); //cacheItem.ValidUntilUtc - _clock.UtcNow;
|
||||
if (maxAge.TotalMilliseconds < 0) {
|
||||
maxAge = TimeSpan.FromSeconds(0);
|
||||
}
|
||||
|
||||
response.Cache.SetCacheability(HttpCacheability.Public);
|
||||
response.Cache.SetMaxAge(maxAge);
|
||||
}
|
||||
|
||||
// an ETag is a string that uniquely identifies a specific version of a component.
|
||||
// we use the cache item to detect if it's a new one
|
||||
response.Cache.SetETag(cacheItem.GetHashCode().ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
response.Cache.SetOmitVaryStar(true);
|
||||
|
||||
if (_varyQueryStringParameters != null) {
|
||||
foreach (var queryStringParam in _varyQueryStringParameters) {
|
||||
response.Cache.VaryByParams[queryStringParam] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// different tenants with the same urls have different entries
|
||||
response.Cache.VaryByHeaders["HOST"] = true;
|
||||
|
||||
// Set the Vary: Accept-Encoding response header.
|
||||
// This instructs the proxies to cache two versions of the resource: one compressed, and one uncompressed.
|
||||
// The correct version of the resource is delivered based on the client request header.
|
||||
// This is a good choice for applications that are singly homed and depend on public proxies for user locality.
|
||||
response.Cache.VaryByHeaders["Accept-Encoding"] = true;
|
||||
|
||||
// create a unique cache per browser, in case a Theme is rendered differently (e.g., mobile)
|
||||
// c.f. http://msdn.microsoft.com/en-us/library/aa478965.aspx
|
||||
// c.f. http://stackoverflow.com/questions/6007287/outputcache-varybyheader-user-agent-or-varybycustom-browser
|
||||
response.Cache.SetVaryByCustom("browser");
|
||||
|
||||
// enabling this would create an entry for each different browser sub-version
|
||||
// response.Cache.VaryByHeaders.UserAgent = true;
|
||||
|
||||
}
|
||||
|
||||
private string ComputeCacheKey(ControllerContext controllerContext, IEnumerable<KeyValuePair<string, object>> parameters) {
|
||||
var url = controllerContext.HttpContext.Request.RawUrl;
|
||||
if(!VirtualPathUtility.IsAbsolute(url)) {
|
||||
var applicationRoot = controllerContext.HttpContext.Request.ToRootUrlString();
|
||||
if(url.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) {
|
||||
url = url.Substring(applicationRoot.Length);
|
||||
}
|
||||
}
|
||||
return ComputeCacheKey(_shellSettings.Name, url, () => _workContext.CurrentCulture, _themeManager.GetRequestTheme(controllerContext.RequestContext).Id, parameters);
|
||||
}
|
||||
|
||||
private string ComputeCacheKey(string tenant, string absoluteUrl, Func<string> culture, string theme, IEnumerable<KeyValuePair<string, object>> parameters){
|
||||
var keyBuilder = new StringBuilder();
|
||||
|
||||
keyBuilder.Append("tenant=").Append(tenant).Append(";");
|
||||
|
||||
keyBuilder.Append("url=").Append(absoluteUrl.ToLowerInvariant()).Append(";");
|
||||
|
||||
// include the theme in the cache key
|
||||
if (_applyCulture) {
|
||||
keyBuilder.Append("culture=").Append(culture().ToLowerInvariant()).Append(";");
|
||||
}
|
||||
|
||||
// include the theme in the cache key
|
||||
keyBuilder.Append("theme=").Append(theme.ToLowerInvariant()).Append(";");
|
||||
|
||||
// include the theme in the cache key
|
||||
keyBuilder.Append("action=").Append(_actionName.ToLowerInvariant()).Append(";");
|
||||
|
||||
if (parameters != null) {
|
||||
foreach (var pair in parameters) {
|
||||
keyBuilder.AppendFormat("{0}={1};", pair.Key.ToLowerInvariant(), Convert.ToString(pair.Value).ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
|
||||
return keyBuilder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the given url should be ignored, as defined in the settings
|
||||
/// </summary>
|
||||
private static bool IsIgnoredUrl(string url, string ignoredUrls)
|
||||
{
|
||||
if(String.IsNullOrEmpty(ignoredUrls))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove ~ if present
|
||||
if(url.StartsWith("~")) {
|
||||
url = url.Substring(1);
|
||||
}
|
||||
|
||||
using (var urlReader = new StringReader(ignoredUrls))
|
||||
{
|
||||
string relativePath;
|
||||
while (null != (relativePath = urlReader.ReadLine()))
|
||||
{
|
||||
// remove ~ if present
|
||||
if (relativePath.StartsWith("~")) {
|
||||
relativePath = relativePath.Substring(1);
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(relativePath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
relativePath = relativePath.Trim();
|
||||
|
||||
// ignore comments
|
||||
if(relativePath.StartsWith("#"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(String.Equals(relativePath, url, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the response stream while writing to it
|
||||
/// </summary>
|
||||
public class CapturingResponseFilter : Stream
|
||||
{
|
||||
private readonly Stream _sink;
|
||||
private readonly MemoryStream _mem;
|
||||
|
||||
public CapturingResponseFilter(Stream sink)
|
||||
{
|
||||
_sink = sink;
|
||||
_mem = new MemoryStream();
|
||||
}
|
||||
|
||||
// The following members of Stream must be overriden.
|
||||
public override bool CanRead
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public override bool CanSeek
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override bool CanWrite
|
||||
{
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public override long Length
|
||||
{
|
||||
get { return 0; }
|
||||
}
|
||||
|
||||
public override long Position { get; set; }
|
||||
|
||||
public override long Seek(long offset, SeekOrigin direction)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public override void SetLength(long length)
|
||||
{
|
||||
_sink.SetLength(length);
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
_sink.Close();
|
||||
_mem.Close();
|
||||
}
|
||||
|
||||
public override void Flush()
|
||||
{
|
||||
_sink.Flush();
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
return _sink.Read(buffer, offset, count);
|
||||
}
|
||||
|
||||
// Override the Write method to filter Response to a file.
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
//Here we will not write to the sink b/c we want to capture
|
||||
_sink.Write(buffer, offset, count);
|
||||
|
||||
//Write out the response to the file.
|
||||
_mem.Write(buffer, 0, count);
|
||||
}
|
||||
|
||||
public string GetContents(Encoding enc)
|
||||
{
|
||||
var buffer = new byte[_mem.Length];
|
||||
_mem.Position = 0;
|
||||
_mem.Read(buffer, 0, buffer.Length);
|
||||
return enc.GetString(buffer, 0, buffer.Length);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public class ViewDataContainer : IViewDataContainer
|
||||
{
|
||||
public ViewDataDictionary ViewData { get; set; }
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
using System.Globalization;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.OutputCache.Services;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Data;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
|
||||
namespace Orchard.OutputCache.Handlers {
|
||||
public class CacheSettingsPartHandler : ContentHandler {
|
||||
private readonly ICacheService _cacheService;
|
||||
|
||||
public CacheSettingsPartHandler(
|
||||
IRepository<CacheSettingsPartRecord> repository,
|
||||
ICacheService cacheService) {
|
||||
_cacheService = cacheService;
|
||||
Filters.Add(new ActivatingFilter<CacheSettingsPart>("Site"));
|
||||
Filters.Add(StorageFilter.For(repository));
|
||||
|
||||
// initializing default cache settings values
|
||||
OnInitializing<CacheSettingsPart>((context, part) => { part.DefaultCacheDuration = 300; });
|
||||
|
||||
// evict modified routable content when updated
|
||||
OnPublished<IContent>((context, part) => Invalidate(part));
|
||||
}
|
||||
|
||||
private void Invalidate(IContent content) {
|
||||
// remove any page tagged with this content item id
|
||||
_cacheService.RemoveByTag(content.ContentItem.Id.ToString(CultureInfo.InvariantCulture));
|
||||
|
||||
// search the cache for containers too
|
||||
var commonPart = content.As<CommonPart>();
|
||||
if (commonPart != null) {
|
||||
if (commonPart.Container != null) {
|
||||
_cacheService.RemoveByTag(commonPart.Container.Id.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Orchard.OutputCache.Services;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
|
||||
namespace Orchard.OutputCache.Handlers {
|
||||
/// <summary>
|
||||
/// Saves references to content items which have been displayed during a request
|
||||
/// </summary>
|
||||
public class DisplayedContentItemHandler : ContentHandler, IDisplayedContentItemHandler {
|
||||
private readonly Collection<int> _itemIds = new Collection<int>();
|
||||
|
||||
protected override void BuildDisplayShape(BuildDisplayContext context) {
|
||||
_itemIds.Add(context.Content.Id);
|
||||
}
|
||||
|
||||
public bool IsDisplayed(int id) {
|
||||
return _itemIds.Contains(id);
|
||||
}
|
||||
|
||||
public IEnumerable<int> GetDisplayed() {
|
||||
return _itemIds.Distinct();
|
||||
}
|
||||
}
|
||||
}
|
84
src/Orchard.Web/Modules/Orchard.OutputCache/Migrations.cs
Normal file
84
src/Orchard.Web/Modules/Orchard.OutputCache/Migrations.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using Orchard.Data.Migration;
|
||||
|
||||
namespace Orchard.OutputCache {
|
||||
public class Migrations : DataMigrationImpl {
|
||||
public int Create() {
|
||||
|
||||
SchemaBuilder.CreateTable("CacheSettingsPartRecord",
|
||||
table => table
|
||||
.ContentPartRecord()
|
||||
.Column<int>("DefaultCacheDuration")
|
||||
.Column<int>("DefaultMaxAge")
|
||||
.Column<string>("IgnoredUrls", c => c.Unlimited())
|
||||
.Column<bool>("DebugMode", c => c.WithDefault(false))
|
||||
.Column<bool>("ApplyCulture", c => c.WithDefault(false))
|
||||
);
|
||||
|
||||
SchemaBuilder.CreateTable("CacheParameterRecord",
|
||||
table => table
|
||||
.Column<int>("Id", c => c.PrimaryKey().Identity())
|
||||
.Column<int>("Duration")
|
||||
.Column<int>("MaxAge")
|
||||
.Column<string>("RouteKey", c => c.WithLength(255))
|
||||
);
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
public int UpdateFrom1() {
|
||||
SchemaBuilder.CreateTable("CacheParameterRecord",
|
||||
table => table
|
||||
.Column<int>("Id", c => c.PrimaryKey().Identity())
|
||||
.Column<int>("Duration")
|
||||
.Column<string>("RouteKey", c => c.WithLength(255))
|
||||
);
|
||||
|
||||
SchemaBuilder.AlterTable("CacheSettingsPartRecord",
|
||||
table => table
|
||||
.AddColumn<string>("IgnoredUrls", c => c.Unlimited())
|
||||
);
|
||||
|
||||
SchemaBuilder.AlterTable("CacheSettingsPartRecord",
|
||||
table => table
|
||||
.AddColumn<bool>("DebugMode", c => c.WithDefault(false))
|
||||
);
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
public int UpdateFrom2() {
|
||||
|
||||
SchemaBuilder.AlterTable("CacheSettingsPartRecord",
|
||||
table => table
|
||||
.AddColumn<bool>("ApplyCulture", c => c.WithDefault(false))
|
||||
);
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
public int UpdateFrom3() {
|
||||
|
||||
SchemaBuilder.AlterTable("CacheSettingsPartRecord",
|
||||
table => table
|
||||
.AddColumn<int>("DefaultMaxAge")
|
||||
);
|
||||
|
||||
SchemaBuilder.AlterTable("CacheParameterRecord",
|
||||
table => table
|
||||
.AddColumn<int>("MaxAge")
|
||||
);
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
public int UpdateFrom4() {
|
||||
|
||||
SchemaBuilder.AlterTable("CacheSettingsPartRecord",
|
||||
table => table
|
||||
.AddColumn<string>("VaryQueryStringParameters", c => c.Unlimited())
|
||||
);
|
||||
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace Orchard.OutputCache.Models {
|
||||
[Serializable]
|
||||
public class CacheItem {
|
||||
public int ValidFor { get; set; }
|
||||
public DateTime CachedOnUtc { get; set; }
|
||||
public string Output { get; set; }
|
||||
public string ContentType { get; set; }
|
||||
public string QueryString { get; set; }
|
||||
public string CacheKey { get; set; }
|
||||
public string InvariantCacheKey { get; set; }
|
||||
public string Url { get; set; }
|
||||
public string Tenant { get; set; }
|
||||
public int StatusCode { get; set; }
|
||||
public string[] Tags { get; set; }
|
||||
|
||||
public DateTime ValidUntilUtc {
|
||||
get { return CachedOnUtc.AddSeconds(ValidFor); }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
namespace Orchard.OutputCache.Models
|
||||
{
|
||||
public class CacheParameterRecord
|
||||
{
|
||||
public virtual int Id { get; set; }
|
||||
public virtual string RouteKey { get; set; }
|
||||
public virtual int Duration { get; set; }
|
||||
public virtual int MaxAge { get; set; }
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
using Orchard.ContentManagement;
|
||||
|
||||
namespace Orchard.OutputCache.Models {
|
||||
public class CacheSettingsPart : ContentPart<CacheSettingsPartRecord> {
|
||||
public const string CacheKey = "CacheSettingsPart";
|
||||
|
||||
public int DefaultCacheDuration {
|
||||
get { return Record.DefaultCacheDuration; }
|
||||
set { Record.DefaultCacheDuration = value; }
|
||||
}
|
||||
|
||||
public int DefaultMaxAge {
|
||||
get { return Record.DefaultMaxAge; }
|
||||
set { Record.DefaultMaxAge = value; }
|
||||
}
|
||||
|
||||
public string VaryQueryStringParameters {
|
||||
get { return Record.VaryQueryStringParameters; }
|
||||
set { Record.VaryQueryStringParameters = value; }
|
||||
}
|
||||
|
||||
public string IgnoredUrls {
|
||||
get { return Record.IgnoredUrls; }
|
||||
set { Record.IgnoredUrls = value; }
|
||||
}
|
||||
|
||||
public bool ApplyCulture {
|
||||
get { return Record.ApplyCulture; }
|
||||
set { Record.ApplyCulture = value; }
|
||||
}
|
||||
|
||||
public bool DebugMode {
|
||||
get { return Record.DebugMode; }
|
||||
set { Record.DebugMode = value; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
using Orchard.ContentManagement.Records;
|
||||
using Orchard.Data.Conventions;
|
||||
|
||||
namespace Orchard.OutputCache.Models {
|
||||
public class CacheSettingsPartRecord : ContentPartRecord {
|
||||
public virtual int DefaultCacheDuration { get; set; }
|
||||
public virtual int DefaultMaxAge { get; set; }
|
||||
public virtual bool DebugMode { get; set; }
|
||||
public virtual bool ApplyCulture { get; set; }
|
||||
|
||||
[StringLengthMax]
|
||||
public virtual string VaryQueryStringParameters { get; set; }
|
||||
|
||||
[StringLengthMax]
|
||||
public virtual string IgnoredUrls { get; set; }
|
||||
}
|
||||
}
|
11
src/Orchard.Web/Modules/Orchard.OutputCache/Module.txt
Normal file
11
src/Orchard.Web/Modules/Orchard.OutputCache/Module.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Name: Output Cache
|
||||
AntiForgery: enabled
|
||||
Author: Sébastien Ros
|
||||
Website: http://orchardproject.net/
|
||||
Version: 1.0
|
||||
OrchardVersion: 1.0
|
||||
Description: Adds Output Caching functionality.
|
||||
Features:
|
||||
Orchard.OutputCache:
|
||||
Description: Adds Output Caching functionality.
|
||||
Category: Performance
|
@@ -0,0 +1,162 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProductVersion>9.0.30729</ProductVersion>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectGuid>{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}</ProjectGuid>
|
||||
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Orchard.OutputCache</RootNamespace>
|
||||
<AssemblyName>Orchard.OutputCache</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
|
||||
<MvcBuildViews>false</MvcBuildViews>
|
||||
<FileUpgradeFlags>
|
||||
</FileUpgradeFlags>
|
||||
<OldToolsVersion>4.0</OldToolsVersion>
|
||||
<UpgradeBackupLocation>
|
||||
</UpgradeBackupLocation>
|
||||
<TargetFrameworkProfile />
|
||||
<UseIISExpress>false</UseIISExpress>
|
||||
<IISExpressSSLPort />
|
||||
<IISExpressAnonymousAuthentication />
|
||||
<IISExpressWindowsAuthentication />
|
||||
<IISExpressUseClassicPipelineMode />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Autofac, Version=2.5.2.830, Culture=neutral, PublicKeyToken=17863af14b0044da, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\autofac\Autofac.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Web.DynamicData" />
|
||||
<Reference Include="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Web" />
|
||||
<Reference Include="System.Web.Abstractions" />
|
||||
<Reference Include="System.Web.Routing" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Web.config" />
|
||||
<Content Include="Views\Web.config" />
|
||||
<Content Include="Scripts\Web.config" />
|
||||
<Content Include="Styles\Web.config" />
|
||||
<Content Include="Properties\AssemblyInfo.cs" />
|
||||
<Content Include="Module.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Orchard\Orchard.Framework.csproj">
|
||||
<Project>{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}</Project>
|
||||
<Name>Orchard.Framework</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Core\Orchard.Core.csproj">
|
||||
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
|
||||
<Name>Orchard.Core</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AdminMenu.cs" />
|
||||
<Compile Include="Controllers\AdminController.cs" />
|
||||
<Compile Include="Controllers\StatisticsController.cs" />
|
||||
<Compile Include="Filters\OutputCacheFilter.cs" />
|
||||
<Compile Include="Handlers\CacheSettingsPartHandler.cs" />
|
||||
<Compile Include="Handlers\DisplayedContentItemHandler.cs" />
|
||||
<Compile Include="Migrations.cs" />
|
||||
<Compile Include="Models\CacheItem.cs" />
|
||||
<Compile Include="Models\CacheSettingsPart.cs" />
|
||||
<Compile Include="Models\CacheSettingsPartRecord.cs" />
|
||||
<Compile Include="Models\CacheParameterRecord.cs" />
|
||||
<Compile Include="Services\CacheService.cs" />
|
||||
<Compile Include="Services\DefaultTagCache.cs" />
|
||||
<Compile Include="Services\IDisplayedContentItemHandler.cs" />
|
||||
<Compile Include="Services\ITagCache.cs" />
|
||||
<Compile Include="Services\DefaultCacheStorageProvider.cs" />
|
||||
<Compile Include="Services\ICacheService.cs" />
|
||||
<Compile Include="Services\IOutputCacheStorageProvider.cs" />
|
||||
<Compile Include="ViewModels\StatisticsViewModel.cs" />
|
||||
<Compile Include="ViewModels\IndexViewModel.cs" />
|
||||
<Compile Include="ViewModels\RouteConfiguration.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Statistics\Index.cshtml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Admin\Index.cshtml" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target> -->
|
||||
<Target Name="AfterBuild" DependsOnTargets="AfterBuildCompiler">
|
||||
<PropertyGroup>
|
||||
<AreasManifestDir>$(ProjectDir)\..\Manifests</AreasManifestDir>
|
||||
</PropertyGroup>
|
||||
<!-- If this is an area child project, uncomment the following line:
|
||||
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Child" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
|
||||
-->
|
||||
<!-- If this is an area parent project, uncomment the following lines:
|
||||
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Parent" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
|
||||
<CopyAreaManifests ManifestPath="$(AreasManifestDir)" CrossCopy="false" RenameViews="true" />
|
||||
-->
|
||||
</Target>
|
||||
<Target Name="AfterBuildCompiler" Condition="'$(MvcBuildViews)'=='true'">
|
||||
<AspNetCompiler VirtualPath="temp" PhysicalPath="$(ProjectDir)\..\$(ProjectName)" />
|
||||
</Target>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
|
||||
<WebProjectProperties>
|
||||
<UseIIS>False</UseIIS>
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>45979</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>
|
||||
</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>True</UseCustomServer>
|
||||
<CustomServerUrl>http://orchard.codeplex.com</CustomServerUrl>
|
||||
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||
</WebProjectProperties>
|
||||
</FlavorProperties>
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
@@ -0,0 +1,34 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("Orchard.OutputCache")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyProduct("Orchard")]
|
||||
[assembly: AssemblyCopyright("Copyright <20> Outercurve Foundation 2013")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("cc5ee19a-8a16-49cf-96a8-5a1bcdd4fa7d")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Revision and Build Numbers
|
||||
// by using the '*' as shown below:
|
||||
[assembly: AssemblyVersion("1.0")]
|
||||
[assembly: AssemblyFileVersion("1.0")]
|
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<add key="webpages:Enabled" value="false" />
|
||||
</appSettings>
|
||||
<system.web>
|
||||
<httpHandlers>
|
||||
<!-- iis6 - for any request in this location, return via managed static file handler -->
|
||||
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
|
||||
</httpHandlers>
|
||||
</system.web>
|
||||
<system.webServer>
|
||||
<handlers accessPolicy="Script,Read">
|
||||
<!--
|
||||
iis7 - for any request to a file exists on disk, return it via native http module.
|
||||
accessPolicy 'Script' is to allow for a managed 404 page.
|
||||
-->
|
||||
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
|
||||
</handlers>
|
||||
</system.webServer>
|
||||
</configuration>
|
@@ -0,0 +1,121 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using System.Web.Routing;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.OutputCache.ViewModels;
|
||||
using Orchard;
|
||||
using Orchard.Caching;
|
||||
using Orchard.Data;
|
||||
using Orchard.Utility.Extensions;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public class CacheService : ICacheService {
|
||||
private readonly IWorkContextAccessor _workContextAccessor;
|
||||
private readonly IRepository<CacheParameterRecord> _repository;
|
||||
private readonly ICacheManager _cacheManager;
|
||||
private readonly IOutputCacheStorageProvider _cacheStorageProvider;
|
||||
private readonly ITagCache _tagCache;
|
||||
private readonly ISignals _signals;
|
||||
|
||||
public CacheService(
|
||||
IWorkContextAccessor workContextAccessor,
|
||||
IRepository<CacheParameterRecord> repository,
|
||||
ICacheManager cacheManager,
|
||||
IOutputCacheStorageProvider cacheStorageProvider,
|
||||
ITagCache tagCache,
|
||||
ISignals signals) {
|
||||
_workContextAccessor = workContextAccessor;
|
||||
_repository = repository;
|
||||
_cacheManager = cacheManager;
|
||||
_cacheStorageProvider = cacheStorageProvider;
|
||||
_tagCache = tagCache;
|
||||
_signals = signals;
|
||||
}
|
||||
|
||||
public void RemoveByTag(string tag) {
|
||||
Collection<string> itemKeys;
|
||||
if (_tagCache.TryGetValue(tag, out itemKeys)) {
|
||||
foreach (var key in itemKeys) {
|
||||
_cacheStorageProvider.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<CacheItem> GetCacheItems() {
|
||||
var workContext = _workContextAccessor.GetContext();
|
||||
|
||||
foreach (DictionaryEntry cacheEntry in workContext.HttpContext.Cache) {
|
||||
var cacheItem = cacheEntry.Value as CacheItem;
|
||||
if (cacheItem != null) {
|
||||
yield return cacheItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Evict(string cacheKey) {
|
||||
var workContext = _workContextAccessor.GetContext();
|
||||
workContext.HttpContext.Cache.Remove(cacheKey);
|
||||
}
|
||||
|
||||
public string GetRouteDescriptorKey(HttpContextBase httpContext, RouteBase routeBase) {
|
||||
var route = routeBase as Route;
|
||||
|
||||
var dataTokens = route != null ? route.DataTokens : routeBase.GetRouteData(httpContext).DataTokens;
|
||||
|
||||
var keyBuilder = new StringBuilder();
|
||||
|
||||
if (route != null) {
|
||||
keyBuilder.AppendFormat("url={0};", route.Url);
|
||||
}
|
||||
|
||||
// the data tokens are used in case the same url is used by several features, like *{path} (Rewrite Rules and Home Page Provider)
|
||||
if (dataTokens != null) {
|
||||
foreach (var key in dataTokens.Keys) {
|
||||
keyBuilder.AppendFormat("{0}={1};", key, dataTokens[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return keyBuilder.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
public CacheParameterRecord GetCacheParameterByKey(string key) {
|
||||
return _repository.Get(c => c.RouteKey == key);
|
||||
}
|
||||
|
||||
public IEnumerable<RouteConfiguration> GetRouteConfigurations() {
|
||||
return _cacheManager.Get("GetRouteConfigurations",
|
||||
ctx => {
|
||||
ctx.Monitor(_signals.When("GetRouteConfigurations"));
|
||||
return _repository.Fetch(c => true).Select(c => new RouteConfiguration { RouteKey = c.RouteKey, Duration = c.Duration }).ToReadOnlyCollection();
|
||||
});
|
||||
}
|
||||
|
||||
public void SaveCacheConfigurations(IEnumerable<RouteConfiguration> routeConfigurations) {
|
||||
// remove all current configurations
|
||||
var configurations = _repository.Fetch(c => true);
|
||||
|
||||
foreach (var configuration in configurations) {
|
||||
_repository.Delete(configuration);
|
||||
}
|
||||
|
||||
// save the new configurations
|
||||
foreach (var configuration in routeConfigurations) {
|
||||
if (!configuration.Duration.HasValue) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_repository.Create(new CacheParameterRecord {
|
||||
Duration = configuration.Duration.Value,
|
||||
RouteKey = configuration.RouteKey
|
||||
});
|
||||
}
|
||||
|
||||
// invalidate the cache
|
||||
_signals.Trigger("GetRouteConfigurations");
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard;
|
||||
using Orchard.Environment.Configuration;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public class DefaultCacheStorageProvider : IOutputCacheStorageProvider {
|
||||
private readonly string _tenantName;
|
||||
private readonly WorkContext _workContext;
|
||||
|
||||
public DefaultCacheStorageProvider(IWorkContextAccessor workContextAccessor, ShellSettings shellSettings) {
|
||||
_workContext = workContextAccessor.GetContext();
|
||||
_tenantName = shellSettings.Name;
|
||||
}
|
||||
|
||||
public void Set(string key, CacheItem cacheItem) {
|
||||
_workContext.HttpContext.Cache.Add(
|
||||
key,
|
||||
cacheItem,
|
||||
null,
|
||||
System.Web.Caching.Cache.NoAbsoluteExpiration,
|
||||
new TimeSpan(0, 0, cacheItem.ValidFor),
|
||||
System.Web.Caching.CacheItemPriority.Normal,
|
||||
null);
|
||||
}
|
||||
|
||||
public void Remove(string key) {
|
||||
_workContext.HttpContext.Cache.Remove(key);
|
||||
}
|
||||
|
||||
public void RemoveAll() {
|
||||
var items = GetCacheItems(0, 100).ToList();
|
||||
while (items.Any()) {
|
||||
foreach (var item in items) {
|
||||
Remove(item.CacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CacheItem GetCacheItem(string key) {
|
||||
return _workContext.HttpContext.Cache.Get(key) as CacheItem;
|
||||
}
|
||||
|
||||
public IEnumerable<CacheItem> GetCacheItems(int skip, int count) {
|
||||
// the ASP.NET cache can also contain other types of items
|
||||
return _workContext.HttpContext.Cache.AsParallel()
|
||||
.Cast<DictionaryEntry>()
|
||||
.Select(x => x.Value)
|
||||
.OfType<CacheItem>()
|
||||
.Where(x => x.Tenant.Equals(_tenantName, StringComparison.OrdinalIgnoreCase))
|
||||
.Skip(skip)
|
||||
.Take(count);
|
||||
}
|
||||
|
||||
public int GetCacheItemsCount() {
|
||||
return _workContext.HttpContext.Cache.AsParallel()
|
||||
.Cast<DictionaryEntry>()
|
||||
.Select(x => x.Value)
|
||||
.OfType<CacheItem>()
|
||||
.Count(x => x.Tenant.Equals(_tenantName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.ObjectModel;
|
||||
using Orchard.OutputCache.Models;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
/// <summary>
|
||||
/// Tenant wide case insensitive reverse index for <see cref="CacheItem"/> tags.
|
||||
/// </summary>
|
||||
public class DefaultTagCache : ConcurrentDictionary<string, Collection<string>>, ITagCache {
|
||||
public DefaultTagCache() : base(StringComparer.OrdinalIgnoreCase) {
|
||||
}
|
||||
|
||||
public void Tag(string tag, params string[] keys) {
|
||||
var collection = GetOrAdd(tag, x => new Collection<string>());
|
||||
|
||||
foreach (var key in keys) {
|
||||
if (!collection.Contains(key)) {
|
||||
collection.Add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Web;
|
||||
using System.Web.Routing;
|
||||
using Orchard.OutputCache.ViewModels;
|
||||
using Orchard;
|
||||
using Orchard.OutputCache.Models;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public interface ICacheService : IDependency {
|
||||
/// <summary>
|
||||
/// Returns the parameters for a specific route
|
||||
/// </summary>
|
||||
/// <param name="key">The key representing the route</param>
|
||||
/// <returns>A <see cref="CacheParameterRecord"/> instance for the specified key, or <c>null</c></returns>
|
||||
CacheParameterRecord GetCacheParameterByKey(string key);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all cache entries associated with a specific tag.
|
||||
/// </summary>
|
||||
/// <param name="tag">The tag value.</param>
|
||||
void RemoveByTag(string tag);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the key representing a specific route in the db
|
||||
/// </summary>
|
||||
string GetRouteDescriptorKey(HttpContextBase httpContext, RouteBase route);
|
||||
|
||||
/// <summary>
|
||||
/// Saves a set of <see cref="RouteConfiguration"/> to the database
|
||||
/// </summary>
|
||||
/// <param name="routeConfigurations"></param>
|
||||
void SaveCacheConfigurations(IEnumerable<RouteConfiguration> routeConfigurations);
|
||||
|
||||
/// <summary>
|
||||
/// Returns all defined configurations for specific routes
|
||||
/// </summary>
|
||||
IEnumerable<RouteConfiguration> GetRouteConfigurations();
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public interface IDisplayedContentItemHandler : IDependency {
|
||||
bool IsDisplayed(int id);
|
||||
IEnumerable<int> GetDisplayed();
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public interface IOutputCacheStorageProvider : IDependency {
|
||||
/// <summary>
|
||||
/// Adds a new <see cref="CacheItem"/> or substitute it with a new one if the
|
||||
/// cache key is already used.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key representing the <see cref="CacheItem"/>.</param>
|
||||
/// <param name="cacheItem">The <see cref="CacheItem"/> instance to add to the cache.</param>
|
||||
void Set(string key, CacheItem cacheItem);
|
||||
|
||||
void Remove(string key);
|
||||
void RemoveAll();
|
||||
|
||||
CacheItem GetCacheItem(string key);
|
||||
IEnumerable<CacheItem> GetCacheItems(int skip, int count);
|
||||
int GetCacheItemsCount();
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using Orchard;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public interface ITagCache : IDictionary<string, Collection<string>>, ISingletonDependency {
|
||||
void Tag(string tag, params string[] keys);
|
||||
}
|
||||
}
|
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<add key="webpages:Enabled" value="false" />
|
||||
</appSettings>
|
||||
<system.web>
|
||||
<httpHandlers>
|
||||
<!-- iis6 - for any request in this location, return via managed static file handler -->
|
||||
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
|
||||
</httpHandlers>
|
||||
</system.web>
|
||||
<system.webServer>
|
||||
<handlers accessPolicy="Script,Read">
|
||||
<!--
|
||||
iis7 - for any request to a file exists on disk, return it via native http module.
|
||||
accessPolicy 'Script' is to allow for a managed 404 page.
|
||||
-->
|
||||
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
|
||||
</handlers>
|
||||
</system.webServer>
|
||||
</configuration>
|
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Orchard.OutputCache.ViewModels {
|
||||
public class IndexViewModel {
|
||||
public List<RouteConfiguration> RouteConfigurations { get; set; }
|
||||
[Range(0, int.MaxValue), Required]
|
||||
public int DefaultCacheDuration { get; set; }
|
||||
[Range(0, int.MaxValue), Required]
|
||||
public int DefaultMaxAge { get; set; }
|
||||
public string IgnoredUrls { get; set; }
|
||||
public bool ApplyCulture { get; set; }
|
||||
public bool DebugMode { get; set; }
|
||||
public string VaryQueryStringParameters { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
namespace Orchard.OutputCache.ViewModels {
|
||||
public class RouteConfiguration {
|
||||
public string RouteKey { get; set; }
|
||||
public string Url { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public int? Duration { get; set; }
|
||||
public int? MaxAge { get; set; }
|
||||
public string FeatureName { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using Orchard.OutputCache.Models;
|
||||
|
||||
namespace Orchard.OutputCache.ViewModels {
|
||||
public class StatisticsViewModel {
|
||||
public IEnumerable<CacheItem> CacheItems { get; set; }
|
||||
public dynamic Pager { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
@model Orchard.OutputCache.ViewModels.IndexViewModel
|
||||
|
||||
@{
|
||||
Layout.Title = T("Cache Settings");
|
||||
|
||||
// group configurations by feature name
|
||||
var featureRouteConfigurations = Model.RouteConfigurations
|
||||
.GroupBy(x => x.FeatureName)
|
||||
.ToDictionary(x => x.Key, x => x.Select(y => y));
|
||||
}
|
||||
|
||||
@using (Html.BeginFormAntiForgeryPost())
|
||||
{
|
||||
@Html.ValidationSummary()
|
||||
|
||||
<fieldset>
|
||||
<label>@T("Default Cache Duration")</label>
|
||||
@Html.TextBoxFor(m => m.DefaultCacheDuration, new { @class = "text-small" })
|
||||
<span class="hint">@T("Number of seconds the pages should be kept in cache on the server.")</span>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label>@T("Max Age")</label>
|
||||
@Html.TextBoxFor(m => m.DefaultMaxAge, new { @class = "text-small" })
|
||||
<span class="hint">@T("When defined, a cache-control header with a max-age property will be added. Use this in order to enable kernel cache on IIS.")</span>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label>@T("Vary Query String Parameters")</label>
|
||||
@Html.TextBoxFor(m => m.VaryQueryStringParameters, new { @class = "textMedium" })
|
||||
<span class="hint">@T("When defined, using comma separated values, sets caching to vary via specified query string parameters")</span>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label>@T("Ignored urls")</label>
|
||||
@Html.TextAreaFor(m => m.IgnoredUrls, new { @class = "textMedium" })
|
||||
<span class="hint">@T("This must be a set of relative paths, e.g., /, /About. Please ensure that you enter only one path per line.")</span>
|
||||
<span class="hint">@T("You can add comments by starting the line with #.")</span>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label>@T("Culture")</label>
|
||||
@Html.CheckBoxFor(m => m.ApplyCulture) <label for="@Html.FieldIdFor(m => m.ApplyCulture)" class="forcheckbox">@T("Differentiate cultures")</label>
|
||||
<span class="hint">@T("When checked, the cached content will differ per culture too. For performance reasons leave it unchecked if you website uses only one culture.")</span>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<label>@T("Debug mode")</label>
|
||||
@Html.CheckBoxFor(m => m.DebugMode) <label for="@Html.FieldIdFor(m => m.DebugMode)" class="forcheckbox">@T("Render caching information in cached pages")</label>
|
||||
</fieldset>
|
||||
|
||||
foreach (var feature in featureRouteConfigurations.Keys) {
|
||||
<h2>@T(feature)</h2>
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">@T("Route")</th>
|
||||
<th scope="col">@T("Priority")</th>
|
||||
<th scope="col">@T("Duration")</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@foreach (var routeConfiguration in featureRouteConfigurations[feature]) {
|
||||
var index = Model.RouteConfigurations.IndexOf(routeConfiguration);
|
||||
<tr>
|
||||
<td>@routeConfiguration.Url</td>
|
||||
<td style="width:60px;">@routeConfiguration.Priority</td>
|
||||
<td style="width:500px;">
|
||||
@Html.TextBoxFor(m => m.RouteConfigurations[index].Duration, new { @class = "text-small" })
|
||||
<span class="hint">@T("Leave empty to use default duration, 0 to disable caching on this route.")</span>
|
||||
</td>
|
||||
@Html.HiddenFor(m => m.RouteConfigurations[index].RouteKey)
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
}
|
||||
|
||||
<button class="primaryAction" type="submit">@T("Save")</button>
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
@using Orchard.OutputCache.ViewModels
|
||||
@model StatisticsViewModel
|
||||
|
||||
@{
|
||||
Layout.Title = T("Cache Statistics");
|
||||
}
|
||||
|
||||
@using (Html.BeginFormAntiForgeryPost()) {
|
||||
@Html.ValidationSummary()
|
||||
<div class="manage">@Html.ActionLink(T("Evict All").ToString(), "EvictAll", new { Area = "Orchard.OutputCache", Controller = "Statistics" }, new { @class = "button primaryAction", itemprop = "UnsafeUrl" })</div>
|
||||
}
|
||||
|
||||
<h2>@T("Cached pages")</h2>
|
||||
<table class="items">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">@T("Url")</th>
|
||||
<th scope="col">@T("Cached On")</th>
|
||||
<th scope="col">@T("Cached Until")</th>
|
||||
<th scope="col"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
@foreach (var cacheItem in Model.CacheItems) {
|
||||
<tr>
|
||||
<td><span title="@cacheItem.QueryString">@cacheItem.Url</span></td>
|
||||
<td>@Display.DateTimeRelative(dateTimeUtc: cacheItem.CachedOnUtc)</td>
|
||||
<td>@cacheItem.ValidUntilUtc.ToLocalTime()</td>
|
||||
<td>@Html.ActionLink(T("Evict").Text, "Evict", new { Area = "Orchard.OutputCache", Controller = "Statistics", cacheKey = cacheItem.CacheKey })</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
|
||||
@Display(Model.Pager)
|
||||
|
||||
|
41
src/Orchard.Web/Modules/Orchard.OutputCache/Views/Web.config
Normal file
41
src/Orchard.Web/Modules/Orchard.OutputCache/Views/Web.config
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0"?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<add key="webpages:Enabled" value="false" />
|
||||
</appSettings>
|
||||
<system.web>
|
||||
<httpHandlers>
|
||||
</httpHandlers>
|
||||
|
||||
<!--
|
||||
Enabling request validation in view pages would cause validation to occur
|
||||
after the input has already been processed by the controller. By default
|
||||
MVC performs request validation before a controller processes the input.
|
||||
To change this behavior apply the ValidateInputAttribute to a
|
||||
controller or action.
|
||||
-->
|
||||
<pages
|
||||
validateRequest="false"
|
||||
pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"
|
||||
pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"
|
||||
userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<controls>
|
||||
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" namespace="System.Web.Mvc" tagPrefix="mvc" />
|
||||
</controls>
|
||||
</pages>
|
||||
</system.web>
|
||||
|
||||
<system.webServer>
|
||||
<validation validateIntegratedModeConfiguration="false"/>
|
||||
<handlers>
|
||||
</handlers>
|
||||
</system.webServer>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
|
||||
<bindingRedirect oldVersion="2.0.0.0" newVersion="3.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
41
src/Orchard.Web/Modules/Orchard.OutputCache/Web.config
Normal file
41
src/Orchard.Web/Modules/Orchard.OutputCache/Web.config
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0"?>
|
||||
<configuration>
|
||||
|
||||
<configSections>
|
||||
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
|
||||
<remove name="host" />
|
||||
<remove name="pages" />
|
||||
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
|
||||
<system.web.webPages.razor>
|
||||
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
|
||||
<pages pageBaseType="Orchard.Mvc.ViewEngines.Razor.WebViewPage">
|
||||
<namespaces>
|
||||
<add namespace="System.Web.Mvc" />
|
||||
<add namespace="System.Web.Mvc.Ajax" />
|
||||
<add namespace="System.Web.Mvc.Html" />
|
||||
<add namespace="System.Web.Routing" />
|
||||
<add namespace="System.Linq"/>
|
||||
<add namespace="System.Web.WebPages" />
|
||||
<add namespace="System.Collections.Generic"/>
|
||||
<add namespace="Orchard.Mvc.Html"/>
|
||||
</namespaces>
|
||||
</pages>
|
||||
</system.web.webPages.razor>
|
||||
|
||||
<system.web>
|
||||
<compilation targetFramework="4.0">
|
||||
<assemblies>
|
||||
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||
<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
|
||||
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
|
||||
<add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||
</assemblies>
|
||||
</compilation>
|
||||
</system.web>
|
||||
|
||||
</configuration>
|
@@ -156,6 +156,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.ImageEditor", "Orch
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.MediaLibrary", "Orchard.Web\Modules\Orchard.MediaLibrary\Orchard.MediaLibrary.csproj", "{73A7688A-5BD3-4F7E-ADFA-CE36C5A10E3B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.OutputCache", "Orchard.Web\Modules\Orchard.OutputCache\Orchard.OutputCache.csproj", "{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
CodeCoverage|Any CPU = CodeCoverage|Any CPU
|
||||
@@ -874,6 +876,16 @@ Global
|
||||
{73A7688A-5BD3-4F7E-ADFA-CE36C5A10E3B}.FxCop|Any CPU.Build.0 = Release|Any CPU
|
||||
{73A7688A-5BD3-4F7E-ADFA-CE36C5A10E3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{73A7688A-5BD3-4F7E-ADFA-CE36C5A10E3B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Coverage|Any CPU.Build.0 = Release|Any CPU
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.FxCop|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.FxCop|Any CPU.Build.0 = Release|Any CPU
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -934,6 +946,7 @@ Global
|
||||
{E649EA64-D213-461B-87F7-D67035801443} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
|
||||
{1F0B6B85-8B0B-47CA-899D-F25B4F1B52C3} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
|
||||
{73A7688A-5BD3-4F7E-ADFA-CE36C5A10E3B} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
|
||||
{6E444FF1-A47C-4CF6-BB3F-507C8EBD776D} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
|
||||
{ABC826D4-2FA1-4F2F-87DE-E6095F653810} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
|
||||
{F112851D-B023-4746-B6B1-8D2E5AD8F7AA} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
|
||||
{6CB3EB30-F725-45C0-9742-42599BA8E8D2} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA}
|
||||
|
Reference in New Issue
Block a user