mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2025-10-08 00:14:31 +08:00
Compare commits
1 Commits
issues/774
...
feature/as
Author | SHA1 | Date | |
---|---|---|---|
![]() |
8aa84ef048 |
@@ -13,6 +13,7 @@ using System.Web.Routing;
|
||||
using Orchard.Caching;
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Mvc.Extensions;
|
||||
using Orchard.Mvc.Filters;
|
||||
@@ -24,6 +25,7 @@ using Orchard.UI.Admin;
|
||||
using Orchard.Utility.Extensions;
|
||||
|
||||
namespace Orchard.OutputCache.Filters {
|
||||
[OrchardFeature("Orchard.OutputCacheOld")]
|
||||
public class OutputCacheFilter : FilterProvider, IActionFilter, IResultFilter {
|
||||
|
||||
private static string _refreshKey = "__r";
|
||||
@@ -604,8 +606,4 @@ namespace Orchard.OutputCache.Filters {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class ViewDataContainer : IViewDataContainer {
|
||||
public ViewDataDictionary ViewData { get; set; }
|
||||
}
|
||||
}
|
@@ -2,10 +2,12 @@
|
||||
using Orchard.ContentManagement;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Core.Common.Models;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.OutputCache.Services;
|
||||
|
||||
namespace Orchard.OutputCache.Handlers {
|
||||
[OrchardFeature("Orchard.OutputCacheOld")]
|
||||
public class CacheSettingsPartHandler : ContentHandler {
|
||||
private readonly ICacheService _cacheService;
|
||||
|
||||
|
@@ -3,11 +3,13 @@ using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using Orchard.OutputCache.Services;
|
||||
using Orchard.ContentManagement.Handlers;
|
||||
using Orchard.Environment.Extensions;
|
||||
|
||||
namespace Orchard.OutputCache.Handlers {
|
||||
/// <summary>
|
||||
/// Saves references to content items which have been displayed during a request
|
||||
/// </summary>
|
||||
[OrchardFeature("Orchard.OutputCacheOld")]
|
||||
public class DisplayedContentItemHandler : ContentHandler, IDisplayedContentItemHandler {
|
||||
private readonly Collection<int> _itemIds = new Collection<int>();
|
||||
|
||||
|
@@ -7,6 +7,14 @@ namespace Orchard.OutputCache.Models {
|
||||
public class CacheSettings : ContentPart {
|
||||
public const string CacheKey = "Orchard_OutputCache_CacheSettings";
|
||||
|
||||
public CacheSettings() {
|
||||
DefaultCacheDuration = 300;
|
||||
DefaultCacheGraceTime = 60;
|
||||
VaryByRequestHeaders = new HashSet<string>();
|
||||
VaryByQueryStringParameters = new List<string>();
|
||||
IgnoredUrls = new List<string>();
|
||||
}
|
||||
|
||||
public CacheSettings(CacheSettingsPart part) {
|
||||
DefaultCacheDuration = part.DefaultCacheDuration;
|
||||
DefaultCacheGraceTime = part.DefaultCacheGraceTime;
|
||||
|
@@ -10,6 +10,10 @@ Features:
|
||||
Name: Output Cache
|
||||
Description: Adds output caching functionality.
|
||||
Category: Performance
|
||||
Orchard.OutputCacheOld:
|
||||
Name: Output Cache
|
||||
Description: OLD cache.
|
||||
Category: Performance
|
||||
Orchard.OutputCache.Database:
|
||||
Name: Database Output Cache
|
||||
Description: Activates a provider that stores output cache data in the database.
|
||||
@@ -20,3 +24,8 @@ Features:
|
||||
Description: Activates a provider that stores output cache data in the App_Data folder.
|
||||
Category: Performance
|
||||
Dependencies: Orchard.OutputCache
|
||||
Orchard.OutputCache2:
|
||||
Name: Output Cache 2
|
||||
Description: Adds output caching functionality.
|
||||
Category: Performance
|
||||
Dependencies: Orchard.OutputCache
|
||||
|
@@ -54,6 +54,14 @@
|
||||
<HintPath>..\..\..\..\lib\autofac\Autofac.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="Microsoft.Owin, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\owin\Microsoft.Owin.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Owin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f0ebd12fd5e55cc5, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\owin\Owin.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations">
|
||||
@@ -107,8 +115,13 @@
|
||||
<Compile Include="Models\CacheSettingsPart.cs" />
|
||||
<Compile Include="Models\CacheParameterRecord.cs" />
|
||||
<Compile Include="Services\CacheService.cs" />
|
||||
<Compile Include="Services\MemoryAsyncTagCache.cs" />
|
||||
<Compile Include="Services\IAsyncTagCache.cs" />
|
||||
<Compile Include="Services\MemoryAsyncOutputCacheStorageProvider.cs" />
|
||||
<Compile Include="Services\FileSystemOutputCacheBackgroundTask.cs" />
|
||||
<Compile Include="Services\DatabaseOutputCacheBackgroundTask.cs" />
|
||||
<Compile Include="Services\IAsyncOutputCacheStorageProvider.cs" />
|
||||
<Compile Include="Services\OutputCacheOwinMiddleware.cs" />
|
||||
<Compile Include="Services\FileSystemOutputCacheProvider.cs" />
|
||||
<Compile Include="Services\DatabaseOutputCacheProvider.cs" />
|
||||
<Compile Include="Services\DefaultCacheControlStrategy.cs" />
|
||||
|
@@ -11,8 +11,10 @@ using Orchard;
|
||||
using Orchard.Caching;
|
||||
using Orchard.Data;
|
||||
using Orchard.Utility.Extensions;
|
||||
using Orchard.Environment.Extensions;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
[OrchardFeature("Orchard.OutputCacheOld")]
|
||||
public class CacheService : ICacheService {
|
||||
private const string RouteConfigsCacheKey = "OutputCache_RouteConfigs";
|
||||
|
||||
|
@@ -4,8 +4,10 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Environment.Extensions;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
[OrchardFeature("Orchard.OutputCacheOld")]
|
||||
public class DefaultCacheStorageProvider : IOutputCacheStorageProvider {
|
||||
private readonly string _tenantName;
|
||||
private readonly WorkContext _workContext;
|
||||
|
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.Utility.Extensions;
|
||||
|
||||
@@ -9,6 +10,7 @@ namespace Orchard.OutputCache.Services {
|
||||
/// <summary>
|
||||
/// Tenant wide case insensitive reverse index for <see cref="CacheItem"/> tags.
|
||||
/// </summary>
|
||||
[OrchardFeature("Orchard.OutputCacheOld")]
|
||||
public class DefaultTagCache : ITagCache {
|
||||
|
||||
private readonly ConcurrentDictionary<string, HashSet<string>> _dictionary;
|
||||
|
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Orchard.OutputCache.Models;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public interface IAsyncOutputCacheStorageProvider : IDependency {
|
||||
Task<CacheItem> GetAsync(string key);
|
||||
Task SetAsync(string key, CacheItem cacheItem);
|
||||
Task RemoveAsync(string key);
|
||||
Task<IEnumerable<CacheItem>> GetAllAsync(int skip, int count);
|
||||
Task RemoveAllAsync();
|
||||
Task<int> CountAsync();
|
||||
}
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
public interface IAsyncTagCache : ISingletonDependency {
|
||||
Task TagAsync(string tag, params string[] keys);
|
||||
Task<IEnumerable<string>> GetTaggedItemsAsync(string tag);
|
||||
Task RemoveTagAsync(string tag);
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.Environment.Configuration;
|
||||
using System.Threading.Tasks;
|
||||
using Orchard.Environment.Extensions;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
[OrchardFeature("Orchard.OutputCache2")]
|
||||
public class MemoryAsyncOutputCacheStorageProvider : IAsyncOutputCacheStorageProvider {
|
||||
private readonly string _tenantName;
|
||||
private readonly WorkContext _workContext;
|
||||
|
||||
public MemoryAsyncOutputCacheStorageProvider(IWorkContextAccessor workContextAccessor, ShellSettings shellSettings) {
|
||||
_workContext = workContextAccessor.GetContext();
|
||||
_tenantName = shellSettings.Name;
|
||||
}
|
||||
|
||||
public async Task SetAsync(string key, CacheItem cacheItem) {
|
||||
_workContext.HttpContext.Cache.Add(
|
||||
key,
|
||||
cacheItem,
|
||||
null,
|
||||
cacheItem.StoredUntilUtc,
|
||||
System.Web.Caching.Cache.NoSlidingExpiration,
|
||||
System.Web.Caching.CacheItemPriority.Normal,
|
||||
null);
|
||||
}
|
||||
|
||||
public async Task<CacheItem> GetAsync(string key) {
|
||||
return _workContext.HttpContext.Cache.Get(key) as CacheItem;
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(string key) {
|
||||
_workContext.HttpContext.Cache.Remove(key);
|
||||
}
|
||||
|
||||
public async Task RemoveAllAsync() {
|
||||
var items = (await GetAllAsync(0, 100)).ToList();
|
||||
while (items.Any()) {
|
||||
foreach (var item in items) {
|
||||
await RemoveAsync(item.CacheKey);
|
||||
}
|
||||
items = (await GetAllAsync(0, 100)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<IEnumerable<CacheItem>> GetAllAsync(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 async Task<int> CountAsync() {
|
||||
return _workContext.HttpContext.Cache.AsParallel()
|
||||
.Cast<DictionaryEntry>()
|
||||
.Select(x => x.Value)
|
||||
.OfType<CacheItem>()
|
||||
.Count(x => x.Tenant.Equals(_tenantName, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Utility.Extensions;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
/// <summary>
|
||||
/// Tenant wide case insensitive reverse index for <see cref="CacheItem"/> tags.
|
||||
/// </summary>
|
||||
[OrchardFeature("Orchard.OutputCache2")]
|
||||
public class MemoryAsyncTagCache : IAsyncTagCache {
|
||||
|
||||
private readonly ConcurrentDictionary<string, HashSet<string>> _dictionary;
|
||||
|
||||
public MemoryAsyncTagCache() {
|
||||
_dictionary = new ConcurrentDictionary<string, HashSet<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public async Task TagAsync(string tag, params string[] keys) {
|
||||
var set = _dictionary.GetOrAdd(tag, x => new HashSet<string>());
|
||||
|
||||
lock (set) {
|
||||
foreach (var key in keys) {
|
||||
set.Add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> GetTaggedItemsAsync(string tag) {
|
||||
HashSet<string> set;
|
||||
if (_dictionary.TryGetValue(tag, out set)) {
|
||||
lock (set) {
|
||||
return set.ToReadOnlyCollection();
|
||||
}
|
||||
}
|
||||
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
public async Task RemoveTagAsync(string tag) {
|
||||
HashSet<string> set;
|
||||
_dictionary.TryRemove(tag, out set);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,695 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Services;
|
||||
using Orchard.FileSystems.AppData;
|
||||
using Orchard.Environment.Configuration;
|
||||
using System.Web;
|
||||
using Orchard.Owin;
|
||||
using Orchard.Environment;
|
||||
using Owin;
|
||||
using Orchard.OutputCache.Filters;
|
||||
using Orchard.Caching;
|
||||
using Orchard.Themes;
|
||||
using Microsoft.Owin;
|
||||
using Orchard.UI.Admin;
|
||||
using System.Web.Routing;
|
||||
using Orchard.ContentManagement;
|
||||
using System.Threading;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Globalization;
|
||||
using Orchard.Utility.Extensions;
|
||||
using System.Web.Mvc;
|
||||
using Orchard.Mvc.Extensions;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace Orchard.OutputCache.Services {
|
||||
|
||||
[OrchardFeature("Orchard.OutputCache2")]
|
||||
public class OutputCacheOwinMiddleware : IOwinMiddlewareProvider {
|
||||
private readonly IWorkContextAccessor _wca;
|
||||
private readonly Work<ICacheManager> _cacheManager;
|
||||
private readonly Work<IAsyncTagCache> _tagCache;
|
||||
private readonly Work<IAsyncOutputCacheStorageProvider> _cacheStorageProvider;
|
||||
//private readonly Work<IDisplayedContentItemHandler> _displayedContentItemHandler;
|
||||
private readonly Work<IWorkContextAccessor> _workContextAccessor;
|
||||
private readonly Work<IThemeManager> _themeManager;
|
||||
private readonly Work<IClock> _clock;
|
||||
//private readonly Work<ICacheService> _cacheService;
|
||||
private readonly Work<ISignals> _signals;
|
||||
private readonly Work<ShellSettings> _shellSettings;
|
||||
|
||||
private static string _refreshKey = "__r";
|
||||
private static long _epoch = new DateTime(2014, DateTimeKind.Utc).Ticks;
|
||||
|
||||
public OutputCacheOwinMiddleware(
|
||||
IWorkContextAccessor wca,
|
||||
Work<ICacheManager> cacheManager,
|
||||
Work<IAsyncOutputCacheStorageProvider> cacheStorageProvider,
|
||||
Work<IAsyncTagCache> tagCache,
|
||||
//Work<IDisplayedContentItemHandler> displayedContentItemHandler,
|
||||
Work<IWorkContextAccessor> workContextAccessor,
|
||||
Work<IThemeManager> themeManager,
|
||||
Work<IClock> clock,
|
||||
//Work<ICacheService> cacheService,
|
||||
Work<ISignals> signals,
|
||||
Work<ShellSettings> shellSettings) {
|
||||
|
||||
_cacheManager = cacheManager;
|
||||
_cacheStorageProvider = cacheStorageProvider;
|
||||
_tagCache = tagCache;
|
||||
//_displayedContentItemHandler = displayedContentItemHandler;
|
||||
_workContextAccessor = workContextAccessor;
|
||||
_themeManager = themeManager;
|
||||
_clock = clock;
|
||||
//_cacheService = cacheService;
|
||||
_signals = signals;
|
||||
_shellSettings = shellSettings;
|
||||
|
||||
_wca = wca;
|
||||
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public IEnumerable<OwinMiddlewareRegistration> GetOwinMiddlewares() {
|
||||
return new[]
|
||||
{
|
||||
// Although we only construct a single OwinMiddlewareRegistration here, you could return multiple ones of course.
|
||||
new OwinMiddlewareRegistration
|
||||
{
|
||||
// The priority value decides the order of OwinMiddlewareRegistrations. I.e. "0" will run before "10", but registrations
|
||||
// without a priority value will run before the ones that have it set.
|
||||
// Note that this priority notation is the same as the one for shape placement (so you can e.g. use ":before").
|
||||
Priority = "0",
|
||||
|
||||
// This is the delegate that sets up middlewares.
|
||||
Configure = app =>
|
||||
|
||||
app.Use( async (context, next) => {
|
||||
|
||||
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
|
||||
var workContext = _wca.GetContext(httpContext);
|
||||
|
||||
var cacheManager = new DefaultCacheManager(typeof(OutputCacheOwinMiddleware), workContext.Resolve<ICacheHolder>()); // _cacheManager.Value;
|
||||
var cacheStorageProvider = _cacheStorageProvider.Value;
|
||||
//var displayedContentItemHandler = _displayedContentItemHandler.Value;
|
||||
var themeManager = _themeManager.Value;
|
||||
var clock = _clock.Value;
|
||||
//var cacheService = _cacheService.Value;
|
||||
var signals = _signals.Value;
|
||||
var tagCache = _tagCache.Value;
|
||||
var shellSettings = _shellSettings.Value;
|
||||
|
||||
var cacheSettings = cacheManager.Get(CacheSettings.CacheKey, ctx => {
|
||||
ctx.Monitor(signals.When(CacheSettings.CacheKey));
|
||||
// return new CacheSettings(workContext.CurrentSite.As<CacheSettingsPart>());
|
||||
return new CacheSettings();
|
||||
});
|
||||
|
||||
// State.
|
||||
DateTime now;
|
||||
string cacheKey;
|
||||
string invariantCacheKey;
|
||||
bool transformRedirect;
|
||||
bool isCachingRequest;
|
||||
bool isCacheableRequest;
|
||||
|
||||
#region OnActionExecuting
|
||||
Logger.Debug("Incoming request for URL '{0}'.", httpContext.Request.RawUrl);
|
||||
|
||||
// This filter is not reentrant (multiple executions within the same request are
|
||||
// not supported) so child actions are ignored completely.
|
||||
//if (filterContext.IsChildAction) {
|
||||
// Logger.Debug("Action '{0}' ignored because it's a child action.", filterContext.ActionDescriptor.ActionName);
|
||||
// return;
|
||||
//}
|
||||
|
||||
now = clock.UtcNow;
|
||||
|
||||
isCacheableRequest = RequestIsCacheable(context, workContext, cacheSettings);
|
||||
|
||||
// if the request can't be cached, process normally
|
||||
if(!isCacheableRequest) {
|
||||
await next.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
// Computing the cache key after we know that the request is cacheable means that we are only performing this calculation on requests that require it
|
||||
cacheKey = ComputeCacheKey(context, shellSettings, GetCacheKeyParameters(context, themeManager, workContext, cacheSettings));
|
||||
invariantCacheKey = ComputeCacheKey(context, shellSettings, null);
|
||||
|
||||
Logger.Debug("Cache key '{0}' was created.", cacheKey);
|
||||
|
||||
|
||||
try {
|
||||
|
||||
// Is there a cached item, and are we allowed to serve it?
|
||||
var allowServeFromCache = httpContext.Request.Headers["Cache-Control"] != "no-cache" || cacheSettings.IgnoreNoCache;
|
||||
var cacheItem = await GetCacheItemAsync(cacheKey, cacheStorageProvider);
|
||||
if (allowServeFromCache && cacheItem != null) {
|
||||
|
||||
Logger.Debug("Item '{0}' was found in cache.", cacheKey);
|
||||
|
||||
// Is the cached item in its grace period?
|
||||
if (cacheItem.IsInGracePeriod(now)) {
|
||||
|
||||
// Render the content unless another request is already doing so.
|
||||
if (Monitor.TryEnter(String.Intern(cacheKey))) {
|
||||
Logger.Debug("Item '{0}' is in grace period and not currently being rendered; rendering item...", cacheKey);
|
||||
BeginRenderItem(context, cacheSettings, out isCachingRequest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Cached item is not yet in its grace period, or is already being
|
||||
// rendered by another request; serve it from cache.
|
||||
Logger.Debug("Serving item '{0}' from cache.", cacheKey);
|
||||
await ServeCachedItemAsync(context, cacheSettings, cacheItem);
|
||||
return;
|
||||
}
|
||||
|
||||
// No cached item found, or client doesn't want it; acquire the cache key
|
||||
// lock to render the item.
|
||||
Logger.Debug("Item '{0}' was not found in cache or client refuses it. Acquiring cache key lock...", cacheKey);
|
||||
if (Monitor.TryEnter(String.Intern(cacheKey), TimeSpan.FromSeconds(20))) {
|
||||
Logger.Debug("Cache key lock for item '{0}' was acquired.", cacheKey);
|
||||
|
||||
// Item might now have been rendered and cached by another request; if so serve it from cache.
|
||||
if (allowServeFromCache) {
|
||||
cacheItem = await GetCacheItemAsync(cacheKey, cacheStorageProvider);
|
||||
if (cacheItem != null) {
|
||||
Logger.Debug("Item '{0}' was now found; releasing cache key lock and serving from cache.", cacheKey);
|
||||
Monitor.Exit(String.Intern(cacheKey));
|
||||
await ServeCachedItemAsync(context, cacheSettings, cacheItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Either we acquired the cache key lock and the item was still not in cache, or
|
||||
// the lock acquisition timed out. In either case render the item.
|
||||
Logger.Debug("Rendering item '{0}'...", cacheKey);
|
||||
BeginRenderItem(context, cacheSettings, out isCachingRequest);
|
||||
|
||||
}
|
||||
catch {
|
||||
// Remember to release the cache key lock in the event of an exception!
|
||||
Logger.Debug("Exception occurred for item '{0}'; releasing any acquired lock.", cacheKey);
|
||||
if (Monitor.IsEntered(String.Intern(cacheKey)))
|
||||
Monitor.Exit(String.Intern(cacheKey));
|
||||
throw;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
//var response = httpContext.Response;
|
||||
//var captureStream = new CaptureStream(response.Filter);
|
||||
//response.Filter = captureStream;
|
||||
//captureStream.Captured += (output) => {
|
||||
// cacheItem = new CacheItem() {
|
||||
// Output = output,
|
||||
// ContentType = response.ContentType,
|
||||
// StatusCode = response.StatusCode,
|
||||
// Duration = 300
|
||||
// };
|
||||
|
||||
// using (var scope = _wca.CreateWorkContextScope()) {
|
||||
// var appDataFolder = scope.Resolve<IAppDataFolder>();
|
||||
// var shellSettings = scope.Resolve<ShellSettings>();
|
||||
// var filename = appDataFolder.Combine("OutputCache", shellSettings.Name, HttpUtility.UrlEncode(url));
|
||||
// using (var stream = Serialize(cacheItem)) {
|
||||
// using (var fileStream = appDataFolder.CreateFile(filename)) {
|
||||
// stream.CopyToAsync(fileStream);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//};
|
||||
|
||||
await next.Invoke();
|
||||
|
||||
transformRedirect = await TransformRedirectAsync(context, cacheSettings, shellSettings, now, tagCache, cacheStorageProvider);
|
||||
|
||||
#region OnResultExecuted
|
||||
var captureHandlerIsAttached = false;
|
||||
|
||||
try {
|
||||
|
||||
// This filter is not reentrant (multiple executions within the same request are
|
||||
// not supported) so child actions are ignored completely.
|
||||
//if (filterContext.IsChildAction )
|
||||
// return;
|
||||
|
||||
if(!isCachingRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.Debug("Item '{0}' was rendered.", cacheKey);
|
||||
|
||||
// Obtain individual route configuration, if any.
|
||||
//CacheRouteConfig configuration = null;
|
||||
//var configurations = cacheService.GetRouteConfigs();
|
||||
//if (configurations.Any()) {
|
||||
// var route = filterContext.Controller.ControllerContext.RouteData.Route;
|
||||
// var key = _cacheService.GetRouteDescriptorKey(filterContext.HttpContext, route);
|
||||
// configuration = configurations.FirstOrDefault(c => c.RouteKey == key);
|
||||
//}
|
||||
|
||||
//if (!ResponseIsCacheable(filterContext, configuration)) {
|
||||
// filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
|
||||
// filterContext.HttpContext.Response.Cache.SetNoStore();
|
||||
// filterContext.HttpContext.Response.Cache.SetMaxAge(new TimeSpan(0));
|
||||
// return;
|
||||
//}
|
||||
|
||||
// Determine duration and grace time.
|
||||
//var cacheDuration = configuration != null && configuration.Duration.HasValue ? configuration.Duration.Value : CacheSettings.DefaultCacheDuration;
|
||||
//var cacheGraceTime = configuration != null && configuration.GraceTime.HasValue ? configuration.GraceTime.Value : CacheSettings.DefaultCacheGraceTime;
|
||||
|
||||
// TODO: adapt
|
||||
var cacheDuration = cacheSettings.DefaultCacheDuration;
|
||||
var cacheGraceTime = cacheSettings.DefaultCacheGraceTime;
|
||||
|
||||
|
||||
//// Include each content item ID as tags for the cache entry.
|
||||
//var contentItemIds = _displayedContentItemHandler.GetDisplayed().Select(x => x.ToString(CultureInfo.InvariantCulture)).ToArray();
|
||||
|
||||
//// Capture the response output using a custom filter stream.
|
||||
//var response = filterContext.HttpContext.Response;
|
||||
|
||||
var response = httpContext.Response;
|
||||
var captureStream = new CaptureStream(response.Filter);
|
||||
response.Filter = captureStream;
|
||||
captureStream.Captured += (output) => {
|
||||
try {
|
||||
// Since this is a callback any call to injected dependencies can result in an Autofac exception: "Instances
|
||||
// cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it has already been disposed."
|
||||
// To prevent access to the original lifetime scope a new work context scope should be created here and dependencies
|
||||
// should be resolved from it.
|
||||
|
||||
using (var scope = _wca.CreateWorkContextScope()) {
|
||||
var cacheItem = new CacheItem() {
|
||||
CachedOnUtc = now,
|
||||
Duration = cacheDuration,
|
||||
GraceTime = cacheGraceTime,
|
||||
Output = output,
|
||||
ContentType = response.ContentType,
|
||||
QueryString = httpContext.Request.Url.Query,
|
||||
CacheKey = cacheKey,
|
||||
InvariantCacheKey = invariantCacheKey,
|
||||
Url = httpContext.Request.Url.AbsolutePath,
|
||||
Tenant = scope.Resolve<ShellSettings>().Name,
|
||||
StatusCode = response.StatusCode,
|
||||
Tags = new[] { invariantCacheKey } //.Union(contentItemIds).ToArray()
|
||||
};
|
||||
|
||||
// Write the rendered item to the cache.
|
||||
var localCacheStorageProvider = scope.Resolve<IAsyncOutputCacheStorageProvider>();
|
||||
cacheStorageProvider.RemoveAsync(cacheKey);
|
||||
cacheStorageProvider.SetAsync(cacheKey, cacheItem);
|
||||
|
||||
Logger.Debug("Item '{0}' was written to cache.", cacheKey);
|
||||
|
||||
// Also add the item tags to the tag cache.
|
||||
var localTagCache = scope.Resolve<IAsyncTagCache>();
|
||||
foreach (var tag in cacheItem.Tags) {
|
||||
localTagCache.TagAsync(tag, cacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
// Always release the cache key lock when the request ends.
|
||||
ReleaseCacheKeyLock(cacheKey);
|
||||
}
|
||||
};
|
||||
|
||||
captureHandlerIsAttached = true;
|
||||
}
|
||||
finally {
|
||||
// If the response filter stream capture handler was attached then we'll trust
|
||||
// it to release the cache key lock at some point in the future when the stream
|
||||
// is flushed; otherwise we'll make sure we'll release it here.
|
||||
if (!captureHandlerIsAttached)
|
||||
ReleaseCacheKeyLock(String.Intern(cacheKey));
|
||||
}
|
||||
#endregion
|
||||
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected virtual bool RequestIsCacheable(IOwinContext context, WorkContext workContext, CacheSettings cacheSettings) {
|
||||
|
||||
var itemDescriptor = string.Empty;
|
||||
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
|
||||
|
||||
//if (Logger.IsEnabled(LogLevel.Debug)) {
|
||||
// var url = filterContext.RequestContext.HttpContext.Request.RawUrl;
|
||||
// var area = filterContext.RequestContext.RouteData.Values["area"];
|
||||
// var controller = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
|
||||
// var action = filterContext.ActionDescriptor.ActionName;
|
||||
// var culture = _workContext.CurrentCulture.ToLowerInvariant();
|
||||
// var auth = filterContext.HttpContext.User.Identity.IsAuthenticated.ToString().ToLowerInvariant();
|
||||
// var theme = _themeManager.GetRequestTheme(filterContext.RequestContext).Id.ToLowerInvariant();
|
||||
|
||||
// itemDescriptor = string.Format("{0} (Area: {1}, Controller: {2}, Action: {3}, Culture: {4}, Theme: {5}, Auth: {6})", url, area, controller, action, culture, theme, auth);
|
||||
//}
|
||||
|
||||
// Respect OutputCacheAttribute if applied.
|
||||
//var actionAttributes = filterContext.ActionDescriptor.GetCustomAttributes(typeof(OutputCacheAttribute), true);
|
||||
//var controllerAttributes = filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(typeof(OutputCacheAttribute), true);
|
||||
//var outputCacheAttribute = actionAttributes.Concat(controllerAttributes).Cast<OutputCacheAttribute>().FirstOrDefault();
|
||||
//if (outputCacheAttribute != null) {
|
||||
// if (outputCacheAttribute.Duration <= 0 || outputCacheAttribute.NoStore) {
|
||||
// Logger.Debug("Request for item '{0}' ignored based on OutputCache attribute.", itemDescriptor);
|
||||
// return false;
|
||||
// }
|
||||
//}
|
||||
|
||||
// Don't cache POST requests.
|
||||
if (context.Request.Method.Equals("POST", StringComparison.OrdinalIgnoreCase)) {
|
||||
Logger.Debug("Request for item '{0}' ignored because HTTP method is POST.", itemDescriptor);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't cache admin section requests.
|
||||
if (AdminFilter.IsApplied(new RequestContext(httpContext, new RouteData()))) {
|
||||
Logger.Debug("Request for item '{0}' ignored because it's in admin section.", itemDescriptor);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore authenticated requests unless the setting to cache them is true.
|
||||
if (workContext.CurrentUser != null && !cacheSettings.CacheAuthenticatedRequests) {
|
||||
Logger.Debug("Request for item '{0}' ignored because user is authenticated.", itemDescriptor);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't cache ignored URLs.
|
||||
if (IsIgnoredUrl(httpContext.Request.AppRelativeCurrentExecutionFilePath, cacheSettings.IgnoredUrls)) {
|
||||
Logger.Debug("Request for item '{0}' ignored because the URL is configured as ignored.", itemDescriptor);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ignore requests with the refresh key on the query string.
|
||||
foreach (var key in httpContext.Request.QueryString.AllKeys) {
|
||||
if (String.Equals(_refreshKey, key, StringComparison.OrdinalIgnoreCase)) {
|
||||
Logger.Debug("Request for item '{0}' ignored because refresh key was found on query string.", itemDescriptor);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual bool ResponseIsCacheable(IOwinContext context, CacheRouteConfig configuration, bool transformRedirect, string cacheKey) {
|
||||
|
||||
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
|
||||
|
||||
if (httpContext.Request.Url == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't cache non-200 responses or results of a redirect.
|
||||
var response = httpContext.Response;
|
||||
if (response.StatusCode != 200 || transformRedirect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't cache in individual route configuration says no.
|
||||
if (configuration != null && configuration.Duration == 0) {
|
||||
Logger.Debug("Response for item '{0}' will not be cached because route is configured to not be cached.", cacheKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't cache if request created notifications.
|
||||
//var hasNotifications = !String.IsNullOrEmpty(Convert.ToString(filterContext.Controller.TempData["messages"]));
|
||||
//if (hasNotifications) {
|
||||
// Logger.Debug("Response for item '{0}' will not be cached because one or more notifications were created.", _cacheKey);
|
||||
// return false;
|
||||
//}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual IDictionary<string, object> GetCacheKeyParameters(IOwinContext context, IThemeManager themeManager, WorkContext workContext, CacheSettings cacheSettings) {
|
||||
var result = new Dictionary<string, object>();
|
||||
|
||||
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
|
||||
|
||||
// Vary by action parameters.
|
||||
//foreach (var p in filterContext.ActionParameters)
|
||||
// result.Add("PARAM:" + p.Key, p.Value);
|
||||
|
||||
// Vary by theme.
|
||||
result.Add("theme", themeManager.GetRequestTheme(httpContext.Request.RequestContext).Id.ToLowerInvariant());
|
||||
|
||||
// Vary by configured query string parameters.
|
||||
var queryString = httpContext.Request.RequestContext.HttpContext.Request.QueryString;
|
||||
foreach (var key in queryString.AllKeys) {
|
||||
if (key == null || (cacheSettings.VaryByQueryStringParameters != null && !cacheSettings.VaryByQueryStringParameters.Contains(key)))
|
||||
continue;
|
||||
result[key] = queryString[key];
|
||||
}
|
||||
|
||||
// Vary by configured request headers.
|
||||
var requestHeaders = httpContext.Request.RequestContext.HttpContext.Request.Headers;
|
||||
foreach (var varyByRequestHeader in cacheSettings.VaryByRequestHeaders) {
|
||||
if (requestHeaders.AllKeys.Contains(varyByRequestHeader))
|
||||
result["HEADER:" + varyByRequestHeader] = requestHeaders[varyByRequestHeader];
|
||||
}
|
||||
|
||||
|
||||
// Vary by request culture if configured.
|
||||
if (cacheSettings.VaryByCulture) {
|
||||
result["culture"] = workContext.CurrentCulture.ToLowerInvariant();
|
||||
}
|
||||
|
||||
// Vary by authentication state if configured.
|
||||
if (cacheSettings.VaryByAuthenticationState) {
|
||||
result["auth"] = httpContext.User.Identity.IsAuthenticated.ToString().ToLowerInvariant();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected virtual async Task<bool> TransformRedirectAsync(IOwinContext context, CacheSettings cacheSettings, ShellSettings shellSettings, DateTime now, IAsyncTagCache tagCache, IAsyncOutputCacheStorageProvider cacheStorageProvider) {
|
||||
|
||||
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
|
||||
|
||||
// Removes the target of the redirection from cache after a POST.
|
||||
|
||||
//if (filterContext.Result == null) {
|
||||
// throw new ArgumentNullException();
|
||||
//}
|
||||
|
||||
if (AdminFilter.IsApplied(new RequestContext(httpContext, new RouteData()))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RedirectResult redirectResult = null; // filterContext.Result as RedirectResult;
|
||||
|
||||
// status code can't be tested at this point, so test the result type instead
|
||||
if (redirectResult == null || !httpContext.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.Debug("Redirect on POST detected; removing from cache and adding refresh key.");
|
||||
|
||||
var redirectUrl = redirectResult.Url;
|
||||
|
||||
if (httpContext.Request.IsLocalUrl(redirectUrl)) {
|
||||
// Remove all cached versions of the same item.
|
||||
var helper = new UrlHelper(httpContext.Request.RequestContext);
|
||||
var absolutePath = new Uri(helper.MakeAbsolute(redirectUrl)).AbsolutePath;
|
||||
var invariantCacheKey = ComputeCacheKey(shellSettings.Name, absolutePath, null);
|
||||
|
||||
foreach (var key in await tagCache.GetTaggedItemsAsync(invariantCacheKey)) {
|
||||
await cacheStorageProvider.RemoveAsync(key);
|
||||
}
|
||||
|
||||
// we no longer need the tag entry as the items have been removed
|
||||
await tagCache.RemoveTagAsync(invariantCacheKey);
|
||||
|
||||
}
|
||||
|
||||
// Adding a refresh key so that the redirection doesn't get restored
|
||||
// from a cached version on a proxy. This can happen when using public
|
||||
// caching, we want to force the client to get a fresh copy of the
|
||||
// redirectUrl content.
|
||||
|
||||
if (cacheSettings.DefaultMaxAge > 0) {
|
||||
var epIndex = redirectUrl.IndexOf('?');
|
||||
var qs = new NameValueCollection();
|
||||
if (epIndex > 0) {
|
||||
qs = HttpUtility.ParseQueryString(redirectUrl.Substring(epIndex));
|
||||
}
|
||||
|
||||
// Substract Epoch to get a smaller number.
|
||||
var refresh = now.Ticks - _epoch;
|
||||
qs.Remove(_refreshKey);
|
||||
|
||||
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.Permanent);
|
||||
|
||||
//filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task ServeCachedItemAsync(IOwinContext context, CacheSettings cacheSettings, CacheItem cacheItem) {
|
||||
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
|
||||
|
||||
// Fix for missing charset in response headers
|
||||
// context.Response.Charset = response.Charset;
|
||||
|
||||
// Adds some caching information to the output if requested.
|
||||
if (cacheSettings.DebugMode) {
|
||||
//response.AddHeader("X-Cached-On", cacheItem.CachedOnUtc.ToString("r"));
|
||||
//response.AddHeader("X-Cached-Until", cacheItem.ValidUntilUtc.ToString("r"));
|
||||
}
|
||||
|
||||
context.Response.ContentType = cacheItem.ContentType;
|
||||
|
||||
await context.Response.WriteAsync(cacheItem.Output);
|
||||
|
||||
|
||||
context.Response.StatusCode = cacheItem.StatusCode;
|
||||
|
||||
ApplyCacheControl(context, cacheSettings);
|
||||
}
|
||||
|
||||
private void BeginRenderItem(IOwinContext context, CacheSettings cacheSettings, out bool isCachingRequest) {
|
||||
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
|
||||
|
||||
var response = httpContext.Response;
|
||||
|
||||
ApplyCacheControl(context, cacheSettings);
|
||||
|
||||
// Remember that we should intercept the rendered response output.
|
||||
isCachingRequest = true;
|
||||
}
|
||||
|
||||
private void ApplyCacheControl(IOwinContext context, CacheSettings cacheSettings) {
|
||||
|
||||
if (cacheSettings.DefaultMaxAge > 0) {
|
||||
var maxAge = TimeSpan.FromSeconds(cacheSettings.DefaultMaxAge); //cacheItem.ValidUntilUtc - _clock.UtcNow;
|
||||
if (maxAge.TotalMilliseconds < 0) {
|
||||
maxAge = TimeSpan.Zero;
|
||||
}
|
||||
//response.Cache.SetCacheability(HttpCacheability.Public);
|
||||
//response.Cache.SetMaxAge(maxAge);
|
||||
}
|
||||
|
||||
// Keeping this example for later usage.
|
||||
// response.DisableUserCache();
|
||||
// response.DisableKernelCache();
|
||||
// response.Cache.SetOmitVaryStar(true);
|
||||
|
||||
// An ETag is a string that uniquely identifies a specific version of a component.
|
||||
// We use the cache item to detect if it's a new one.
|
||||
if (HttpRuntime.UsingIntegratedPipeline) {
|
||||
//if (response.Headers.Get("ETag") == null) {
|
||||
// // What is the point of GetHashCode() of a newly generated item? /DanielStolt
|
||||
// response.Cache.SetETag(new CacheItem().GetHashCode().ToString(CultureInfo.InvariantCulture));
|
||||
//}
|
||||
}
|
||||
|
||||
//if (cacheSettings.VaryByQueryStringParameters == null) {
|
||||
// response.Cache.VaryByParams["*"] = true;
|
||||
//}
|
||||
//else {
|
||||
// foreach (var queryStringParam in cacheSettings.VaryByQueryStringParameters) {
|
||||
// response.Cache.VaryByParams[queryStringParam] = true;
|
||||
// }
|
||||
//}
|
||||
|
||||
//foreach (var varyRequestHeader in cacheSettings.VaryByRequestHeaders) {
|
||||
// response.Cache.VaryByHeaders[varyRequestHeader] = true;
|
||||
//}
|
||||
}
|
||||
|
||||
private void ReleaseCacheKeyLock(string cacheKey) {
|
||||
if (cacheKey != null) {
|
||||
if (Monitor.IsEntered(String.Intern(cacheKey))) {
|
||||
Logger.Debug("Releasing cache key lock for item '{0}'.", cacheKey);
|
||||
Monitor.Exit(String.Intern(cacheKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool IsIgnoredUrl(string url, IEnumerable<string> ignoredUrls) {
|
||||
if (ignoredUrls == null || !ignoredUrls.Any()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
url = url.TrimStart(new[] { '~' });
|
||||
|
||||
foreach (var ignoredUrl in ignoredUrls) {
|
||||
var relativePath = ignoredUrl.TrimStart(new[] { '~' }).Trim();
|
||||
if (String.IsNullOrWhiteSpace(relativePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore comments
|
||||
if (relativePath.StartsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (String.Equals(relativePath, url, StringComparison.OrdinalIgnoreCase)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected virtual string ComputeCacheKey(IOwinContext context, ShellSettings shellSettings, IEnumerable<KeyValuePair<string, object>> parameters) {
|
||||
var httpContext = context.Environment["System.Web.HttpContextBase"] as HttpContextBase;
|
||||
var url = httpContext.Request.Url.AbsolutePath;
|
||||
return ComputeCacheKey(shellSettings.Name, url, parameters);
|
||||
}
|
||||
|
||||
protected virtual string ComputeCacheKey(string tenant, string absoluteUrl, IEnumerable<KeyValuePair<string, object>> parameters) {
|
||||
var keyBuilder = new StringBuilder();
|
||||
|
||||
keyBuilder.AppendFormat("tenant={0};url={1};", tenant, absoluteUrl.ToLowerInvariant());
|
||||
|
||||
if (parameters != null) {
|
||||
foreach (var pair in parameters) {
|
||||
keyBuilder.AppendFormat("{0}={1};", pair.Key.ToLowerInvariant(), Convert.ToString(pair.Value).ToLowerInvariant());
|
||||
}
|
||||
}
|
||||
|
||||
return keyBuilder.ToString();
|
||||
}
|
||||
|
||||
protected virtual async Task<CacheItem> GetCacheItemAsync(string key, IAsyncOutputCacheStorageProvider cacheStorageProvider) {
|
||||
try {
|
||||
var cacheItem = await cacheStorageProvider.GetAsync(key);
|
||||
return cacheItem;
|
||||
}
|
||||
catch (Exception e) {
|
||||
Logger.Error(e, "An unexpected error occured while reading a cache entry");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -25,3 +25,8 @@ Features:
|
||||
Description: Business data cache using Redis.
|
||||
Category: Performance
|
||||
Dependencies: Orchard.Caching, Orchard.Redis
|
||||
Orchard.Redis.OutputCache2:
|
||||
Name: Redis Output Cache 2
|
||||
Description: An output cache storage provider using Redis.
|
||||
Category: Performance
|
||||
Dependencies: Orchard.OutputCache2, Orchard.Redis
|
@@ -111,7 +111,9 @@
|
||||
<Compile Include="Configuration\IRedisConnectionProvider.cs" />
|
||||
<Compile Include="Configuration\RedisConnectionProvider.cs" />
|
||||
<Compile Include="MessageBus\RedisMessageBusBroker.cs" />
|
||||
<Compile Include="OutputCache\RedisAsyncOutputCacheStorageProvider.cs" />
|
||||
<Compile Include="OutputCache\RedisOutputCacheStorageProvider.cs" />
|
||||
<Compile Include="OutputCache\RedisAsyncTagCache.cs" />
|
||||
<Compile Include="OutputCache\RedisTagCache.cs" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
|
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Logging;
|
||||
using Orchard.Redis.Configuration;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.OutputCache.Services;
|
||||
using Orchard.Redis.Extensions;
|
||||
using StackExchange.Redis;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Orchard.Redis.OutputCache {
|
||||
|
||||
[OrchardFeature("Orchard.Redis.OutputCache2")]
|
||||
[OrchardSuppressDependency("Orchard.OutputCache.Services.MemoryAsyncOutputCacheStorageProvider")]
|
||||
public class RedisAsyncOutputCacheStorageProvider : IAsyncOutputCacheStorageProvider {
|
||||
|
||||
private readonly ShellSettings _shellSettings;
|
||||
private readonly IRedisConnectionProvider _redisConnectionProvider;
|
||||
private HashSet<string> _keysCache;
|
||||
|
||||
public const string ConnectionStringKey = "Orchard.Redis.OutputCache";
|
||||
private readonly string _connectionString;
|
||||
private readonly ConnectionMultiplexer _connectionMultiplexer;
|
||||
|
||||
public RedisAsyncOutputCacheStorageProvider(ShellSettings shellSettings, IRedisConnectionProvider redisConnectionProvider) {
|
||||
_shellSettings = shellSettings;
|
||||
_redisConnectionProvider = redisConnectionProvider;
|
||||
_connectionString = _redisConnectionProvider.GetConnectionString(ConnectionStringKey);
|
||||
_connectionMultiplexer = _redisConnectionProvider.GetConnection(_connectionString);
|
||||
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public IDatabase Database {
|
||||
get {
|
||||
return _connectionMultiplexer.GetDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetAsync(string key, CacheItem cacheItem) {
|
||||
if(cacheItem == null) {
|
||||
throw new ArgumentNullException("cacheItem");
|
||||
}
|
||||
|
||||
if (cacheItem.ValidFor <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
using (var decompressedStream = Serialize(cacheItem)) {
|
||||
using (var compressedStream = await CompressAsync(decompressedStream)) {
|
||||
await Database.StringSetAsync(GetLocalizedKey(key), compressedStream.ToArray(), TimeSpan.FromSeconds(cacheItem.ValidFor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(string key) {
|
||||
await Database.KeyDeleteAsync(GetLocalizedKey(key));
|
||||
}
|
||||
|
||||
public async Task RemoveAllAsync() {
|
||||
Database.KeyDeleteWithPrefix(GetLocalizedKey("*"));
|
||||
}
|
||||
|
||||
public async Task<CacheItem> GetAsync(string key) {
|
||||
var value = await Database.StringGetAsync(GetLocalizedKey(key));
|
||||
|
||||
if (value.IsNullOrEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
using (var compressedStream = new MemoryStream(value)) {
|
||||
if(compressedStream.Length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
using(var decompressedStream = await DecompressAsync(compressedStream)) {
|
||||
return Deserialize(decompressedStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CacheItem>> GetAllAsync(int skip, int count) {
|
||||
var result = new List<CacheItem>();
|
||||
foreach (var key in GetAllKeys().Skip(skip).Take(count)) {
|
||||
var cacheItem = await GetAsync(key);
|
||||
// the item could have expired in the meantime
|
||||
if (cacheItem != null) {
|
||||
result.Add(cacheItem);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<int> CountAsync() {
|
||||
return Database.KeyCount(GetLocalizedKey("*"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a namespaced key to support multiple tenants on top of a single Redis connection.
|
||||
/// </summary>
|
||||
/// <param name="key">The key to localized.</param>
|
||||
/// <returns>A localized key based on the tenant name.</returns>
|
||||
private string GetLocalizedKey(string key) {
|
||||
return _shellSettings.Name + ":OC:" + CacheItem.Version + ":" + key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all the keys for the current tenant.
|
||||
/// </summary>
|
||||
/// <returns>The keys for the current tenant.</returns>
|
||||
private IEnumerable<string> GetAllKeys() {
|
||||
// prevent the same request from computing the list twice (count + list)
|
||||
if (_keysCache == null) {
|
||||
_keysCache = new HashSet<string>();
|
||||
var prefix = GetLocalizedKey("");
|
||||
|
||||
foreach (var endPoint in _connectionMultiplexer.GetEndPoints()) {
|
||||
var server = _connectionMultiplexer.GetServer(endPoint);
|
||||
foreach (var key in server.Keys(pattern: GetLocalizedKey("*"))) {
|
||||
_keysCache.Add(key.ToString().Substring(prefix.Length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _keysCache;
|
||||
}
|
||||
|
||||
private static MemoryStream Serialize(CacheItem item) {
|
||||
BinaryFormatter binaryFormatter = new BinaryFormatter();
|
||||
var memoryStream = new MemoryStream();
|
||||
binaryFormatter.Serialize(memoryStream, item);
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
return memoryStream;
|
||||
}
|
||||
|
||||
private static CacheItem Deserialize(Stream stream) {
|
||||
BinaryFormatter binaryFormatter = new BinaryFormatter();
|
||||
var result = (CacheItem)binaryFormatter.Deserialize(stream);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<MemoryStream> CompressAsync(Stream stream) {
|
||||
var compressedStream = new MemoryStream();
|
||||
using (var compressionStream = new GZipStream(compressedStream, CompressionMode.Compress)) {
|
||||
await stream.CopyToAsync(compressionStream);
|
||||
return compressedStream;
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<Stream> DecompressAsync(Stream stream) {
|
||||
var decompressedStream = new MemoryStream();
|
||||
using (GZipStream decompressionStream = new GZipStream(stream, CompressionMode.Decompress)) {
|
||||
await decompressionStream.CopyToAsync(decompressedStream);
|
||||
decompressedStream.Seek(0, SeekOrigin.Begin);
|
||||
return decompressedStream;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.OutputCache.Services;
|
||||
using Orchard.Redis.Configuration;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace Orchard.Redis.OutputCache {
|
||||
[OrchardFeature("Orchard.Redis.OutputCache2")]
|
||||
[OrchardSuppressDependency("Orchard.OutputCache.Services.MemoryAsyncTagCache")]
|
||||
public class RedisAsyncTagCache : IAsyncTagCache {
|
||||
private readonly IRedisConnectionProvider _redisConnectionProvider;
|
||||
private readonly string _connectionString;
|
||||
private readonly ConnectionMultiplexer _connectionMultiplexer;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
|
||||
public RedisAsyncTagCache(IRedisConnectionProvider redisConnectionProvider, ShellSettings shellSettings) {
|
||||
_redisConnectionProvider = redisConnectionProvider;
|
||||
_connectionString = _redisConnectionProvider.GetConnectionString(RedisOutputCacheStorageProvider.ConnectionStringKey);
|
||||
_connectionMultiplexer = _redisConnectionProvider.GetConnection(_connectionString);
|
||||
_shellSettings = shellSettings;
|
||||
}
|
||||
|
||||
private IDatabase Database {
|
||||
get { return _connectionMultiplexer.GetDatabase(); }
|
||||
}
|
||||
|
||||
public async Task TagAsync(string tag, params string[] keys) {
|
||||
await Database.SetAddAsync(GetLocalizedKey(tag), Array.ConvertAll(keys, x=> (RedisValue) x));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<string>> GetTaggedItemsAsync(string tag) {
|
||||
var values = await Database.SetMembersAsync(GetLocalizedKey(tag));
|
||||
if (values == null || values.Length == 0)
|
||||
return Enumerable.Empty<string>();
|
||||
return Array.ConvertAll(values, x => (string) x);
|
||||
}
|
||||
|
||||
public async Task RemoveTagAsync(string tag) {
|
||||
await Database.KeyDeleteAsync(GetLocalizedKey(tag));
|
||||
}
|
||||
|
||||
private string GetLocalizedKey(string key) {
|
||||
return _shellSettings.Name + ":Tag:" + key;
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,7 +14,9 @@ namespace Orchard.Owin {
|
||||
if (handler == null) {
|
||||
throw new ArgumentException("orchard.Handler can't be null");
|
||||
}
|
||||
|
||||
await handler();
|
||||
|
||||
});
|
||||
|
||||
return app;
|
||||
|
Reference in New Issue
Block a user