mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Improving output cache
The OutputCache HttpModule is necessary to enable kernel cache. However the current usage won't impact memory usage as pages are not stored in output cache unless Page.InitOutputCache is called in MVC. c.f. https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/OutputCacheAttribute.cs
This commit is contained in:
@@ -21,6 +21,8 @@ using Orchard.Themes;
|
||||
using Orchard.UI.Admin;
|
||||
using Orchard.Utility.Extensions;
|
||||
using System.Collections.Specialized;
|
||||
using Orchard.OutputCache.ViewModels;
|
||||
using Orchard.UI.Admin.Notification;
|
||||
|
||||
namespace Orchard.OutputCache.Filters {
|
||||
public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter {
|
||||
@@ -36,7 +38,10 @@ namespace Orchard.OutputCache.Filters {
|
||||
private readonly ISignals _signals;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
private readonly ICacheControlStrategy _cacheControlStrategy;
|
||||
private Stream _previousFilter;
|
||||
private readonly INotificationManager _notificationManager;
|
||||
|
||||
TextWriter _originalWriter;
|
||||
StringWriter _cachingWriter;
|
||||
|
||||
private static string RefreshKey = "__r";
|
||||
private static long Epoch = new DateTime(2014, DateTimeKind.Utc).Ticks;
|
||||
@@ -52,7 +57,8 @@ namespace Orchard.OutputCache.Filters {
|
||||
ICacheService cacheService,
|
||||
ISignals signals,
|
||||
ShellSettings shellSettings,
|
||||
ICacheControlStrategy cacheControlStrategy
|
||||
ICacheControlStrategy cacheControlStrategy,
|
||||
INotificationManager notificationManager
|
||||
) {
|
||||
_cacheManager = cacheManager;
|
||||
_cacheStorageProvider = cacheStorageProvider;
|
||||
@@ -65,6 +71,7 @@ namespace Orchard.OutputCache.Filters {
|
||||
_signals = signals;
|
||||
_shellSettings = shellSettings;
|
||||
_cacheControlStrategy = cacheControlStrategy;
|
||||
_notificationManager = notificationManager;
|
||||
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
@@ -83,8 +90,8 @@ namespace Orchard.OutputCache.Filters {
|
||||
private bool _transformRedirect;
|
||||
|
||||
private WorkContext _workContext;
|
||||
private CapturingResponseFilter _filter;
|
||||
private CacheItem _cacheItem;
|
||||
private Func<ControllerContext, string> _completeResponse;
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
@@ -285,8 +292,11 @@ namespace Orchard.OutputCache.Filters {
|
||||
ApplyCacheControl(_cacheItem, response);
|
||||
|
||||
// no cache content available, intercept the execution results for caching
|
||||
_previousFilter = response.Filter;
|
||||
response.Filter = _filter = new CapturingResponseFilter();
|
||||
_cachingWriter = new StringWriter(CultureInfo.InvariantCulture);
|
||||
_originalWriter = filterContext.HttpContext.Response.Output;
|
||||
filterContext.HttpContext.Response.Output = _cachingWriter;
|
||||
|
||||
_completeResponse = CaptureResponse;
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext filterContext) {
|
||||
@@ -297,123 +307,51 @@ namespace Orchard.OutputCache.Filters {
|
||||
|
||||
public void OnResultExecuted(ResultExecutedContext filterContext) {
|
||||
|
||||
if (AdminFilter.IsApplied(new RequestContext(filterContext.HttpContext, new RouteData()))) {
|
||||
return;
|
||||
string capturedResponse = null;
|
||||
if (_completeResponse != null) {
|
||||
capturedResponse = _completeResponse(filterContext);
|
||||
}
|
||||
|
||||
var response = filterContext.HttpContext.Response;
|
||||
|
||||
if (!_cacheControlStrategy.IsCacheable(filterContext.Result, response)) {
|
||||
_filter = null;
|
||||
if (_previousFilter != null) {
|
||||
response.Filter = _previousFilter;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ignore error results from cache
|
||||
if (response.StatusCode != (int)HttpStatusCode.OK) {
|
||||
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));
|
||||
|
||||
_filter = null;
|
||||
if (_previousFilter != null) {
|
||||
response.Filter = _previousFilter;
|
||||
}
|
||||
|
||||
// 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
|
||||
// i.e., Comment creation
|
||||
if (filterContext.HttpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase) &&
|
||||
!String.IsNullOrWhiteSpace(filterContext.HttpContext.Response.RedirectLocation)) {
|
||||
|
||||
var url = filterContext.HttpContext.Response.RedirectLocation;
|
||||
if (!VirtualPathUtility.IsAbsolute(url)) {
|
||||
var applicationRoot = new UrlHelper(filterContext.HttpContext.Request.RequestContext).MakeAbsolute("/");
|
||||
if (url.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) {
|
||||
url = url.Substring(applicationRoot.Length);
|
||||
}
|
||||
}
|
||||
|
||||
var redirectionInvariantCacheKey = ComputeCacheKey(_shellSettings.Name, url, () => _workContext.CurrentCulture, _themeManager.GetRequestTheme(filterContext.RequestContext).Id, null);
|
||||
|
||||
_cacheService.RemoveByTag(redirectionInvariantCacheKey);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// ignore in admin
|
||||
if (AdminFilter.IsApplied(new RequestContext(filterContext.HttpContext, new RouteData()))) {
|
||||
_filter = null;
|
||||
if (_previousFilter != null) {
|
||||
response.Filter = _previousFilter;
|
||||
}
|
||||
if (capturedResponse == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_workContext = _workContextAccessor.GetContext();
|
||||
|
||||
// ignore authenticated requests
|
||||
if (_workContext.CurrentUser != null) {
|
||||
_filter = null;
|
||||
if (_previousFilter != null) {
|
||||
response.Filter = _previousFilter;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// save the result only if the content can be intercepted
|
||||
if (_filter == null) return;
|
||||
|
||||
// flush here to force the Filter to get the rendered content
|
||||
if (response.IsClientConnected)
|
||||
response.Flush();
|
||||
|
||||
var output = _filter.GetContents(response.ContentEncoding);
|
||||
|
||||
if (String.IsNullOrWhiteSpace(output)) {
|
||||
return;
|
||||
}
|
||||
|
||||
response.Filter = null;
|
||||
response.Write(output);
|
||||
|
||||
// check if there is a specific rule not to cache the whole route
|
||||
RouteConfiguration configuration = null;
|
||||
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);
|
||||
|
||||
if (configurations.Any()) {
|
||||
var route = filterContext.Controller.ControllerContext.RouteData.Route;
|
||||
var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
|
||||
configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
|
||||
}
|
||||
|
||||
// do not cache ?
|
||||
if (configuration != null && configuration.Duration == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't cache the result of a POST redirection as it could contain notifications
|
||||
if (_transformRedirect) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't cache the result if there were some notifications
|
||||
var messagesZone = _workContextAccessor.GetContext(filterContext).Layout.Zones["Messages"];
|
||||
var hasNotifications = messagesZone != null && ((IEnumerable<dynamic>)messagesZone).Any();
|
||||
if (hasNotifications) {
|
||||
if (_notificationManager.GetNotifications().Any()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// default duration of specific one ?
|
||||
var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : _cacheDuration;
|
||||
|
||||
if (cacheDuration <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// include each of the content item ids as tags for the cache entry
|
||||
var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
|
||||
|
||||
@@ -426,7 +364,7 @@ namespace Orchard.OutputCache.Filters {
|
||||
_cacheItem.CachedOnUtc = _now;
|
||||
_cacheItem.ValidFor = cacheDuration;
|
||||
_cacheItem.QueryString = filterContext.HttpContext.Request.Url.Query;
|
||||
_cacheItem.Output = output;
|
||||
_cacheItem.Output = capturedResponse;
|
||||
_cacheItem.CacheKey = _cacheKey;
|
||||
_cacheItem.InvariantCacheKey = _invariantCacheKey;
|
||||
_cacheItem.Tenant = _shellSettings.Name;
|
||||
@@ -445,7 +383,14 @@ namespace Orchard.OutputCache.Filters {
|
||||
foreach (var tag in _cacheItem.Tags) {
|
||||
_tagCache.Tag(tag, _cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
private string CaptureResponse(ControllerContext filterContext) {
|
||||
filterContext.HttpContext.Response.Output = _originalWriter;
|
||||
|
||||
string capturedText = _cachingWriter.ToString();
|
||||
filterContext.HttpContext.Response.Write(capturedText);
|
||||
return capturedText;
|
||||
}
|
||||
|
||||
private bool TransformRedirect(ActionExecutedContext filterContext) {
|
||||
@@ -469,12 +414,13 @@ namespace Orchard.OutputCache.Filters {
|
||||
}
|
||||
|
||||
Logger.Debug("Redirect on POST");
|
||||
var redirectUrl = redirectResult.Url ;
|
||||
var redirectUrl = redirectResult.Url;
|
||||
|
||||
if (!VirtualPathUtility.IsAbsolute(redirectUrl)) {
|
||||
var applicationRoot = new UrlHelper(filterContext.HttpContext.Request.RequestContext).MakeAbsolute("/");
|
||||
if (redirectUrl.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) {
|
||||
redirectUrl = redirectUrl.Substring(applicationRoot.Length);
|
||||
redirectUrl = "~/" + redirectUrl.Substring(applicationRoot.Length);
|
||||
redirectUrl = VirtualPathUtility.ToAbsolute(redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,7 +518,8 @@ namespace Orchard.OutputCache.Filters {
|
||||
if (!VirtualPathUtility.IsAbsolute(url)) {
|
||||
var applicationRoot = new UrlHelper(controllerContext.HttpContext.Request.RequestContext).MakeAbsolute("/");
|
||||
if (url.StartsWith(applicationRoot, StringComparison.OrdinalIgnoreCase)) {
|
||||
url = url.Substring(applicationRoot.Length);
|
||||
url = "~/" + url.Substring(applicationRoot.Length);
|
||||
url = VirtualPathUtility.ToAbsolute(url);
|
||||
}
|
||||
}
|
||||
return ComputeCacheKey(_shellSettings.Name, url, () => _workContext.CurrentCulture, _themeManager.GetRequestTheme(controllerContext.RequestContext).Id, parameters);
|
||||
@@ -652,82 +599,7 @@ namespace Orchard.OutputCache.Filters {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Captures the response stream while writing to it
|
||||
/// </summary>
|
||||
public class CapturingResponseFilter : Stream {
|
||||
private readonly MemoryStream _mem;
|
||||
|
||||
public CapturingResponseFilter() {
|
||||
_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) {
|
||||
}
|
||||
|
||||
public override void Close() {
|
||||
_mem.Close();
|
||||
}
|
||||
|
||||
public override void Flush() {
|
||||
}
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count) {
|
||||
return count;
|
||||
}
|
||||
|
||||
// Override the Write method to filter Response to a file.
|
||||
public override void Write(byte[] buffer, int offset, int 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 byte[] GetContents() {
|
||||
return _mem.ToArray();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing) {
|
||||
if (disposing) {
|
||||
_mem.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
public class ViewDataContainer : IViewDataContainer {
|
||||
public ViewDataDictionary ViewData { get; set; }
|
||||
}
|
||||
|
||||
@@ -99,7 +99,6 @@
|
||||
<validation validateIntegratedModeConfiguration="false" />
|
||||
|
||||
<modules runAllManagedModulesForAllRequests="false">
|
||||
<remove name="OutputCache" />
|
||||
<remove name="WarmupHttpModule"/>
|
||||
<add name="WarmupHttpModule" type="Orchard.WarmupStarter.WarmupHttpModule, Orchard.WarmupStarter, Version=1.7.2, Culture=neutral"/>
|
||||
</modules>
|
||||
|
||||
Reference in New Issue
Block a user