mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Compare commits
1 Commits
feature/as
...
feature/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
295e5e8785 |
@@ -1,4 +1,5 @@
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Security;
|
||||
using Orchard.UI.Navigation;
|
||||
@@ -24,4 +25,33 @@ namespace Orchard.MultiTenancy {
|
||||
.Permission(StandardPermissions.SiteOwner)));
|
||||
}
|
||||
}
|
||||
|
||||
[OrchardFeature("Orchard.MultiTenancy.Commands")]
|
||||
public class CommandsAdminMenu : INavigationProvider
|
||||
{
|
||||
private readonly ShellSettings _shellSettings;
|
||||
|
||||
public CommandsAdminMenu(ShellSettings shellSettings)
|
||||
{
|
||||
_shellSettings = shellSettings;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public string MenuName
|
||||
{
|
||||
get { return "admin"; }
|
||||
}
|
||||
|
||||
public void GetNavigation(NavigationBuilder builder)
|
||||
{
|
||||
if (_shellSettings.Name != ShellSettings.DefaultName)
|
||||
return;
|
||||
|
||||
builder.Add(T("Tenants"), "90",
|
||||
menu => menu.Add(T("Commands"), "1", item => item.Action("Index", "Commands", new { area = "Orchard.MultiTenancy" })
|
||||
.Permission(StandardPermissions.SiteOwner)));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
using Orchard.Commands;
|
||||
using Orchard.Data;
|
||||
using Orchard.Environment;
|
||||
using Orchard.Environment.Configuration;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.Localization;
|
||||
using Orchard.Logging;
|
||||
using Orchard.MultiTenancy.Services;
|
||||
using Orchard.MultiTenancy.ViewModels;
|
||||
using Orchard.Security;
|
||||
using Orchard.Themes;
|
||||
using Orchard.Tokens;
|
||||
using Orchard.UI.Admin;
|
||||
using Orchard.UI.Notify;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Web.Mvc;
|
||||
|
||||
namespace Orchard.MultiTenancy.Controllers
|
||||
{
|
||||
[Themed, Admin, OrchardFeature("Orchard.MultiTenancy.Commands")]
|
||||
public class CommandsController : Controller
|
||||
{
|
||||
private readonly ITenantService _tenantService;
|
||||
private readonly IOrchardHost _orchardHost;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
|
||||
public CommandsController(
|
||||
IOrchardServices services,
|
||||
ITenantService tenantService,
|
||||
IOrchardHost orchardHost,
|
||||
ShellSettings shellSettings)
|
||||
{
|
||||
_tenantService = tenantService;
|
||||
_orchardHost = orchardHost;
|
||||
_shellSettings = shellSettings;
|
||||
|
||||
Services = services;
|
||||
T = NullLocalizer.Instance;
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public IOrchardServices Services { get; set; }
|
||||
public Localizer T { get; set; }
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public ActionResult Index()
|
||||
{
|
||||
return Execute();
|
||||
}
|
||||
|
||||
public ActionResult Execute()
|
||||
{
|
||||
if (!Services.Authorizer.Authorize(StandardPermissions.SiteOwner))
|
||||
{
|
||||
return new HttpUnauthorizedResult();
|
||||
}
|
||||
|
||||
if (!EnsureDefaultTenant())
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
return View("Execute", new CommandsExecuteViewModel
|
||||
{
|
||||
Tenants= _tenantService.GetTenants()
|
||||
.Where(x => x.State == Environment.Configuration.TenantState.Running)
|
||||
.Select(x => x.Name)
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult Execute(string tenant, string commandLine)
|
||||
{
|
||||
if(!Services.Authorizer.Authorize(StandardPermissions.SiteOwner))
|
||||
{
|
||||
return new HttpUnauthorizedResult();
|
||||
}
|
||||
|
||||
if (!EnsureDefaultTenant())
|
||||
return new HttpUnauthorizedResult();
|
||||
|
||||
string output = "";
|
||||
|
||||
var shellContext = _orchardHost.GetShellContext(tenant);
|
||||
|
||||
if(shellContext == null)
|
||||
{
|
||||
return Json(new
|
||||
{
|
||||
error = T("Tenant not found: {0}", tenant).Text,
|
||||
});
|
||||
}
|
||||
|
||||
if (shellContext.Settings.State != TenantState.Running)
|
||||
{
|
||||
return Json(new
|
||||
{
|
||||
error = T("Tenant not running: {0}", tenant).Text,
|
||||
});
|
||||
}
|
||||
|
||||
using (var workContext = shellContext.LifetimeScope.CreateWorkContextScope())
|
||||
{
|
||||
var commandManager = workContext.Resolve<ICommandManager>();
|
||||
var transactionManager = workContext.Resolve<ITransactionManager>();
|
||||
var tokenizer = workContext.Resolve<ITokenizer>();
|
||||
|
||||
try
|
||||
{
|
||||
using (var writer = new StringWriter())
|
||||
{
|
||||
commandLine = tokenizer.Replace(commandLine, new Dictionary<string, object>());
|
||||
|
||||
var parameters = CommandParser.ParseCommandParameters(commandLine);
|
||||
parameters.Output = writer;
|
||||
commandManager.Execute(parameters);
|
||||
output = writer.ToString();
|
||||
}
|
||||
|
||||
return Json(new
|
||||
{
|
||||
data = output
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Error(T("Error executing command for tenant {0}: {1}", tenant, exception.Message).Text);
|
||||
|
||||
transactionManager.Cancel();
|
||||
return Json(new
|
||||
{
|
||||
error = exception.Message,
|
||||
data = output
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnsureDefaultTenant()
|
||||
{
|
||||
return _shellSettings.Name == ShellSettings.DefaultName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,13 @@ Website: http://orchardproject.net
|
||||
Version: 1.9.1
|
||||
OrchardVersion: 1.9
|
||||
Description: The multi-tenancy module enables multiple Orchard sites to run in isolation inside of a single web application, improving site density on a single server or hosted account.
|
||||
FeatureDescription: Configure multiple site tenants.
|
||||
Category: Hosting
|
||||
Dependencies: Orchard.jQuery
|
||||
Features:
|
||||
Orchard.MultiTenancy:
|
||||
FeatureDescription: Configure multiple site tenants.
|
||||
Category: Hosting
|
||||
Dependencies: Orchard.jQuery
|
||||
Orchard.MultiTenancy.Commands:
|
||||
Name: Multi Tenancy Commands
|
||||
FeatureDescription: Run commands on tenants.
|
||||
Category: Hosting
|
||||
Dependencies: Orchard.Tokens
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<IISExpressAnonymousAuthentication />
|
||||
<IISExpressWindowsAuthentication />
|
||||
<IISExpressUseClassicPipelineMode />
|
||||
<UseGlobalApplicationHostFile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
@@ -48,6 +49,13 @@
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Autofac">
|
||||
<HintPath>..\..\..\..\lib\autofac\Autofac.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Web.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
@@ -59,22 +67,45 @@
|
||||
<Reference Include="System.Web.DynamicData" />
|
||||
<Reference Include="System.Web.Entity" />
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
<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 Include="System.Web.Helpers, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.Helpers.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\..\packages\Microsoft.AspNet.Mvc.5.2.3\lib\net45\System.Web.Mvc.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Web.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\..\packages\Microsoft.AspNet.Razor.3.2.3\lib\net45\System.Web.Razor.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Web.WebPages.Deployment, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Deployment.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\..\packages\Microsoft.AspNet.WebPages.3.2.3\lib\net45\System.Web.WebPages.Razor.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AdminMenu.cs" />
|
||||
<Compile Include="Annotations\SqlDatabaseConnectionStringAttribute.cs" />
|
||||
<Compile Include="Controllers\CommandsController.cs" />
|
||||
<Compile Include="ResourceManifest.cs" />
|
||||
<Compile Include="Commands\TenantCommand.cs" />
|
||||
<Compile Include="Controllers\AdminController.cs" />
|
||||
<Compile Include="Extensions\UrlHelperExtensions.cs" />
|
||||
<Compile Include="Routes.cs" />
|
||||
<Compile Include="Services\CommandParser.cs" />
|
||||
<Compile Include="Services\ITenantService.cs" />
|
||||
<Compile Include="Services\TenantService.cs" />
|
||||
<Compile Include="ViewModels\CommandsExecuteViewModel.cs" />
|
||||
<Compile Include="ViewModels\ModuleEntry.cs" />
|
||||
<Compile Include="ViewModels\TenantEditViewModel.cs" />
|
||||
<Compile Include="ViewModels\TenantAddViewModel.cs" />
|
||||
@@ -102,6 +133,10 @@
|
||||
<Name>Orchard.Framework</Name>
|
||||
<Private>False</Private>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Orchard.Tokens\Orchard.Tokens.csproj">
|
||||
<Project>{6f759635-13d7-4e94-bcc9-80445d63f117}</Project>
|
||||
<Name>Orchard.Tokens</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Content\Web.config">
|
||||
@@ -123,6 +158,9 @@
|
||||
</SubType>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Views\Commands\Execute.cshtml" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
using Orchard.Commands;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
|
||||
namespace Orchard.MultiTenancy.Services
|
||||
{
|
||||
// Utility class for parsing lines of commands.
|
||||
// Note: This lexer handles double quotes pretty harshly by design.
|
||||
// In case you needed them in your arguments, hopefully single quotes work for you as a replacement on the receiving end.
|
||||
public class CommandParser
|
||||
{
|
||||
public static CommandParameters ParseCommandParameters(string command)
|
||||
{
|
||||
var args = SplitArgs(command);
|
||||
var arguments = new List<string>();
|
||||
var result = new CommandParameters
|
||||
{
|
||||
Switches = new Dictionary<string, string>()
|
||||
};
|
||||
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (arg.StartsWith("/"))
|
||||
{
|
||||
//If arg is not empty and starts with '/'
|
||||
|
||||
int index = arg.IndexOf(':');
|
||||
var switchName = (index < 0 ? arg.Substring(1) : arg.Substring(1, index - 1));
|
||||
var switchValue = (index < 0 || index >= arg.Length ? string.Empty : arg.Substring(index + 1));
|
||||
|
||||
if (string.IsNullOrEmpty(switchName))
|
||||
{
|
||||
throw new ArgumentException(string.Format("Invalid switch syntax: \"{0}\". Valid syntax is /<switchName>[:<switchValue>].", arg));
|
||||
}
|
||||
|
||||
result.Switches.Add(switchName, switchValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
arguments.Add(arg);
|
||||
}
|
||||
}
|
||||
|
||||
result.Arguments = arguments;
|
||||
return result;
|
||||
}
|
||||
|
||||
class State
|
||||
{
|
||||
private readonly string _commandLine;
|
||||
private readonly StringBuilder _stringBuilder;
|
||||
private readonly List<string> _arguments;
|
||||
private int _index;
|
||||
|
||||
public State(string commandLine)
|
||||
{
|
||||
_commandLine = commandLine;
|
||||
_stringBuilder = new StringBuilder();
|
||||
_arguments = new List<string>();
|
||||
}
|
||||
|
||||
public StringBuilder StringBuilder { get { return _stringBuilder; } }
|
||||
public bool EOF { get { return _index >= _commandLine.Length; } }
|
||||
public char Current { get { return _commandLine[_index]; } }
|
||||
public IEnumerable<string> Arguments { get { return _arguments; } }
|
||||
|
||||
public void AddArgument()
|
||||
{
|
||||
_arguments.Add(StringBuilder.ToString());
|
||||
StringBuilder.Clear();
|
||||
}
|
||||
|
||||
public void AppendCurrent()
|
||||
{
|
||||
StringBuilder.Append(Current);
|
||||
}
|
||||
|
||||
public void Append(char ch)
|
||||
{
|
||||
StringBuilder.Append(ch);
|
||||
}
|
||||
|
||||
public void MoveNext()
|
||||
{
|
||||
if (!EOF)
|
||||
_index++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implement the same logic as found at
|
||||
/// http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
|
||||
/// The 3 special characters are quote, backslash and whitespaces, in order
|
||||
/// of priority.
|
||||
/// The semantics of a quote is: whatever the state of the lexer, copy
|
||||
/// all characters verbatim until the next quote or EOF.
|
||||
/// The semantics of backslash is: If the next character is a backslash or a quote,
|
||||
/// copy the next character. Otherwise, copy the backslash and the next character.
|
||||
/// The semantics of whitespace is: end the current argument and move on to the next one.
|
||||
/// </summary>
|
||||
private static IEnumerable<string> SplitArgs(string commandLine)
|
||||
{
|
||||
var state = new State(commandLine);
|
||||
while (!state.EOF)
|
||||
{
|
||||
switch (state.Current)
|
||||
{
|
||||
case '"':
|
||||
ProcessQuote(state);
|
||||
break;
|
||||
|
||||
case '\\':
|
||||
ProcessBackslash(state);
|
||||
break;
|
||||
|
||||
case ' ':
|
||||
case '\t':
|
||||
if (state.StringBuilder.Length > 0)
|
||||
state.AddArgument();
|
||||
state.MoveNext();
|
||||
break;
|
||||
|
||||
default:
|
||||
state.AppendCurrent();
|
||||
state.MoveNext();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (state.StringBuilder.Length > 0)
|
||||
state.AddArgument();
|
||||
return state.Arguments;
|
||||
}
|
||||
|
||||
private static void ProcessQuote(State state)
|
||||
{
|
||||
state.MoveNext();
|
||||
while (!state.EOF)
|
||||
{
|
||||
if (state.Current == '"')
|
||||
{
|
||||
state.MoveNext();
|
||||
break;
|
||||
}
|
||||
state.AppendCurrent();
|
||||
state.MoveNext();
|
||||
}
|
||||
|
||||
state.AddArgument();
|
||||
}
|
||||
|
||||
private static void ProcessBackslash(State state)
|
||||
{
|
||||
state.MoveNext();
|
||||
if (state.EOF)
|
||||
{
|
||||
state.Append('\\');
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.Current == '"')
|
||||
{
|
||||
state.Append('"');
|
||||
state.MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
state.Append('\\');
|
||||
state.AppendCurrent();
|
||||
state.MoveNext();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Orchard.MultiTenancy.ViewModels
|
||||
{
|
||||
public class CommandsExecuteViewModel {
|
||||
public IEnumerable<string> Tenants { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
@model Orchard.MultiTenancy.ViewModels.CommandsExecuteViewModel
|
||||
|
||||
@{
|
||||
Script.Require("jQuery").AtFoot();
|
||||
Script.Include(Url.Content("~/Themes/TheAdmin/Scripts/admin.js")).AtFoot();
|
||||
}
|
||||
|
||||
<h1>
|
||||
@Html.TitleForPage(T("Command line").ToString())
|
||||
</h1>
|
||||
<div>
|
||||
<fieldset>
|
||||
<label for="command-line">@T("Command Line")</label>
|
||||
<input id="command-line" type="text" value="" class="text large"/>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<button type="button" id="button-run" class="button">@T("Run")</button>
|
||||
<button type="button" id="button-cancel" class="button" style="display: none">@T("Cancel")</button>
|
||||
<div class="message message-Warning" id="message-progress" style="display: none"></div>
|
||||
</fieldset>
|
||||
|
||||
<div class="tenants">
|
||||
@foreach(var tenant in Model.Tenants) {
|
||||
<fieldset class="tenant" id="tenant-@tenant" data-tenant="@tenant">
|
||||
<label>@tenant</label>
|
||||
<pre class="result"> </pre>
|
||||
</fieldset>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@using (Script.Foot()) {
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
var executeCommandUrl = '@HttpUtility.JavaScriptStringEncode(Url.Action("Execute", "Commands"))';
|
||||
var antiForgeryToken = '@HttpUtility.JavaScriptStringEncode(Html.AntiForgeryTokenValueOrchard().ToString())';
|
||||
var endMessage = '@HttpUtility.JavaScriptStringEncode(T("All tenants have been processed").Text)';
|
||||
var cancelMessage = '@HttpUtility.JavaScriptStringEncode(T("Cancellation requested").Text)';
|
||||
var tenantsRemainingMessage = '@HttpUtility.JavaScriptStringEncode(T("{0} tenants remaining", "{0}").Text)';
|
||||
var tenants = ['@Html.Raw(String.Join("', '", Model.Tenants.Select(HttpUtility.JavaScriptStringEncode)))'];
|
||||
var cancel = false;
|
||||
var isCancelled = function () {
|
||||
return cancel;
|
||||
}
|
||||
|
||||
$('#button-cancel').click(function () {
|
||||
cancel = true;
|
||||
$('#message-progress').text(endMessage);
|
||||
$('#button-cancel').hide();
|
||||
});
|
||||
|
||||
$('#button-run').click(function () {
|
||||
cancel =false;
|
||||
$('#button-cancel').show();
|
||||
$('#button-run').hide();
|
||||
|
||||
var remaining = tenants.length;
|
||||
$('#message-progress').show();
|
||||
|
||||
var deferred = $.Deferred();
|
||||
var tail = deferred.promise();
|
||||
|
||||
tenants.forEach(function (tenant) {
|
||||
console.log("enlisting");
|
||||
|
||||
if (tenant.toLowerCase() == "default") {
|
||||
return;
|
||||
}
|
||||
|
||||
$('#tenant-' + tenant).find("label").removeClass();
|
||||
|
||||
tail = tail.then(function () {
|
||||
if (isCancelled()) {
|
||||
console.log("reject");
|
||||
var t = $.Deferred();
|
||||
t.reject();
|
||||
return t.promise();
|
||||
}
|
||||
|
||||
return $.ajax({
|
||||
type: 'POST',
|
||||
url: executeCommandUrl,
|
||||
// async: false,
|
||||
beforeSend: function (xhr, opts) {
|
||||
console.log("::send");
|
||||
|
||||
var fieldset = $('#tenant-' + tenant);
|
||||
fieldset.find("label").removeClass().addClass("progress");
|
||||
},
|
||||
data: {
|
||||
__RequestVerificationToken: antiForgeryToken,
|
||||
tenant: tenant,
|
||||
commandLine: $('#command-line').val()
|
||||
},
|
||||
success: function (result) {
|
||||
var fieldset = $('#tenant-' + tenant);
|
||||
if (result.error) {
|
||||
fieldset.find("label").removeClass().addClass("fail");
|
||||
fieldset.find("pre").text(result.error);
|
||||
}
|
||||
else {
|
||||
fieldset.find("label").removeClass().addClass("success");
|
||||
fieldset.find("pre").text(result.data);
|
||||
}
|
||||
|
||||
fieldset.find("label").expandoControl(function (controller) { return controller.nextAll("pre"); }, { collapse: true, remember: false });
|
||||
remaining--;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tail
|
||||
|
||||
.fail(function () {
|
||||
$('#message-progress').text(cancelMessage);
|
||||
})
|
||||
.done(function () {
|
||||
$('#message-progress').text(endMessage);
|
||||
})
|
||||
.always(function () {
|
||||
cancel = false;
|
||||
$('#button-run').show();
|
||||
$('#button-cancel').hide();
|
||||
});
|
||||
|
||||
deferred.resolve();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -13,7 +13,6 @@ 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;
|
||||
@@ -25,7 +24,6 @@ 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";
|
||||
@@ -606,4 +604,8 @@ namespace Orchard.OutputCache.Filters {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class ViewDataContainer : IViewDataContainer {
|
||||
public ViewDataDictionary ViewData { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,10 @@
|
||||
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,13 +3,11 @@ 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,14 +7,6 @@ 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,10 +10,6 @@ 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.
|
||||
@@ -24,8 +20,3 @@ 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,14 +54,6 @@
|
||||
<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">
|
||||
@@ -115,13 +107,8 @@
|
||||
<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,10 +11,8 @@ 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,10 +4,8 @@ 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,7 +2,6 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Orchard.Environment.Extensions;
|
||||
using Orchard.OutputCache.Models;
|
||||
using Orchard.Utility.Extensions;
|
||||
|
||||
@@ -10,7 +9,6 @@ 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;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,695 +0,0 @@
|
||||
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,8 +25,3 @@ 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,9 +111,7 @@
|
||||
<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>
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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,9 +14,7 @@ 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