#20881: Changed capture mechanism in Orchard.OutputCache to support any result type.

Instead of using a custom writer for the Response.Output property, the OutputCacheFilter now uses a specialized Stream class for the Response.Filter property, which captures anything written to it. This seems to work with any type of result, including FileContentResult.

Work Item: 20881
This commit is contained in:
Daniel Stolt
2015-03-18 23:10:36 +01:00
parent 53c942637e
commit c9cfb8639c
3 changed files with 131 additions and 54 deletions

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
namespace Orchard.OutputCache.Filters {
public class CaptureStream : Stream {
public CaptureStream(Stream innerStream) {
_innerStream = innerStream;
_captureStream = new MemoryStream();
}
private readonly Stream _innerStream;
private readonly MemoryStream _captureStream;
public override bool CanRead {
get { return _innerStream.CanRead; }
}
public override bool CanSeek {
get { return _innerStream.CanSeek; }
}
public override bool CanWrite {
get { return _innerStream.CanWrite; }
}
public override long Length {
get { return _innerStream.Length; }
}
public override long Position {
get { return _innerStream.Position; }
set { _innerStream.Position = value; }
}
public override long Seek(long offset, SeekOrigin direction) {
return _innerStream.Seek(offset, direction);
}
public override void SetLength(long length) {
_innerStream.SetLength(length);
}
public override void Close() {
_innerStream.Close();
}
public override void Flush() {
if (_captureStream.Length > 0) {
OnCaptured(_captureStream);
_captureStream.SetLength(0);
}
_innerStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count) {
return _innerStream.Read(buffer, offset, count);
}
public override void Write(byte[] buffer, int offset, int count) {
_captureStream.Write(buffer, 0, count);
_innerStream.Write(buffer, offset, buffer.Length);
}
public event Action<string> Captured;
protected virtual void OnCaptured(MemoryStream ms) {
string content = HttpContext.Current.Response.ContentEncoding.GetString(ms.ToArray());
Captured(content);
}
}
}

View File

@@ -19,7 +19,6 @@ 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;
@@ -78,7 +77,7 @@ namespace Orchard.OutputCache.Filters {
private string _cacheKey;
private string _invariantCacheKey;
private bool _transformRedirect;
private Func<ControllerContext, string> _completeResponseFunc;
private bool _isCachingRequest;
public void OnActionExecuting(ActionExecutingContext filterContext) {
@@ -166,18 +165,13 @@ namespace Orchard.OutputCache.Filters {
}
public void OnResultExecuted(ResultExecutedContext filterContext) {
var response = filterContext.HttpContext.Response;
var captureStream = response.Filter as CaptureStream;
if (!_isCachingRequest || captureStream == null)
return;
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.
@@ -203,42 +197,47 @@ namespace Orchard.OutputCache.Filters {
// 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()
};
captureStream.Captured += (content) => {
try {
var cacheItem = new CacheItem() {
CachedOnUtc = _now,
Duration = cacheDuration,
GraceTime = cacheGraceTime,
Output = content,
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);
// Write the rendered item to the cache.
_cacheStorageProvider.Remove(_cacheKey);
_cacheStorageProvider.Set(_cacheKey, cacheItem);
Logger.Debug("Item '{0}' was written to cache.", _cacheKey);
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);
// 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.
ReleaseCacheKeyLock();
}
};
}
catch {
// If an exception is caught, it means we were never able to attach the Captured
// event handler to the CaptureStream instance, because attaching that handler is
// the very last statement in the try block. Hence we can't assume that event
// handler to release the cache key lock, and we should do it here.
ReleaseCacheKeyLock();
throw;
}
}
@@ -464,17 +463,8 @@ namespace Orchard.OutputCache.Filters {
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;
response.Filter = new CaptureStream(response.Filter);
_isCachingRequest = true;
}
private void ApplyCacheControl(HttpResponseBase response) {
@@ -517,6 +507,16 @@ namespace Orchard.OutputCache.Filters {
}
}
private void ReleaseCacheKeyLock() {
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 IsIgnoredUrl(string url, IEnumerable<string> ignoredUrls) {
if (ignoredUrls == null || !ignoredUrls.Any()) {
return false;

View File

@@ -95,6 +95,7 @@
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Controllers\StatisticsController.cs" />
<Compile Include="Filters\CaptureStream.cs" />
<Compile Include="Filters\OutputCacheFilter.cs" />
<Compile Include="Handlers\CacheSettingsPartHandler.cs" />
<Compile Include="Handlers\DisplayedContentItemHandler.cs" />