diff --git a/src/Orchard.Web/Modules/Orchard.Azure/Module.txt b/src/Orchard.Web/Modules/Orchard.Azure/Module.txt
index a3c4bd1e0..db10c6055 100644
--- a/src/Orchard.Web/Modules/Orchard.Azure/Module.txt
+++ b/src/Orchard.Web/Modules/Orchard.Azure/Module.txt
@@ -17,11 +17,6 @@ Features:
Description: Activates an Orchard output cache provider that targets Windows Azure Cache.
Dependencies: Orchard.Azure, Orchard.OutputCache
Category: Performance
- Orchard.Azure.RedisOutputCache:
- Name: Microsoft Azure Redis Output Cache
- Description: Activates an Orchard output cache provider that targets Windows Azure Redis Cache.
- Dependencies: Orchard.Azure, Orchard.OutputCache
- Category: Performance
Orchard.Azure.DatabaseCache:
Name: Microsoft Azure Database Cache
Description: Activates an NHibernate second-level cache provider that targets Microsoft Azure Cache.
diff --git a/src/Orchard.Web/Modules/Orchard.Azure/Orchard.Azure.csproj b/src/Orchard.Web/Modules/Orchard.Azure/Orchard.Azure.csproj
index aa6a2c637..a9fe59a56 100644
--- a/src/Orchard.Web/Modules/Orchard.Azure/Orchard.Azure.csproj
+++ b/src/Orchard.Web/Modules/Orchard.Azure/Orchard.Azure.csproj
@@ -93,9 +93,6 @@
False
..\..\..\..\lib\nhibernate\NHibernate.dll
-
- ..\..\..\..\lib\redis\StackExchange.Redis.dll
-
@@ -111,7 +108,6 @@
-
diff --git a/src/Orchard.Web/Modules/Orchard.Azure/Services/Caching/Output/AzureRedisOutputCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Azure/Services/Caching/Output/AzureRedisOutputCacheStorageProvider.cs
deleted file mode 100644
index 3b1647d94..000000000
--- a/src/Orchard.Web/Modules/Orchard.Azure/Services/Caching/Output/AzureRedisOutputCacheStorageProvider.cs
+++ /dev/null
@@ -1,166 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using Microsoft.ApplicationServer.Caching;
-using Newtonsoft.Json;
-using Orchard.Azure.Services.Environment.Configuration;
-using Orchard.Caching;
-using Orchard.Environment.Configuration;
-using Orchard.Environment.Extensions;
-using Orchard.Logging;
-using Orchard.OutputCache.Models;
-using Orchard.OutputCache.Services;
-using StackExchange.Redis;
-
-namespace Orchard.Azure.Services.Caching.Output {
-
- [OrchardFeature("Orchard.Azure.RedisOutputCache")]
- [OrchardSuppressDependency("Orchard.OutputCache.Services.DefaultCacheStorageProvider")]
- public class AzureRedisCacheStorageProvider : Component, IOutputCacheStorageProvider {
- public const string DataCacheKey = "DataCache";
- public const string ClientConfigurationKey = "CacheClientConfiguration";
-
- private CacheClientConfiguration _cacheClientConfiguration;
- private static ConcurrentDictionary _connectionMultiplexers = new ConcurrentDictionary();
-
- private readonly ICacheManager _cacheManager;
- private readonly ShellSettings _shellSettings;
- private readonly IPlatformConfigurationAccessor _pca;
- private HashSet _keysCache;
-
- public AzureRedisCacheStorageProvider(
- ShellSettings shellSettings,
- IPlatformConfigurationAccessor pca,
- ICacheManager cacheManager) {
- _cacheManager = cacheManager;
- _shellSettings = shellSettings;
- _pca = pca;
-
- Logger = NullLogger.Instance;
- }
-
- public CacheClientConfiguration CacheConfiguration {
- get {
- // the configuration is backed by a field so that we don't call the cacheManager multiple times in the same request
- // cache configurations are stored in the cacheManager so that we don't read the config on each request
- if (_cacheClientConfiguration == null) {
-
- _cacheClientConfiguration = _cacheManager.Get(ClientConfigurationKey, ctx => {
- CacheClientConfiguration cacheConfig;
- try {
- cacheConfig = CacheClientConfiguration.FromPlatformConfiguration(
- _shellSettings.Name,
- Constants.OutputCacheSettingNamePrefix,
- _pca);
-
- cacheConfig.Validate();
- return cacheConfig;
- }
- catch (Exception ex) {
- throw new Exception(String.Format("The {0} configuration settings are missing or invalid.", Constants.OutputCacheFeatureName), ex);
- }
- });
-
- if (_cacheClientConfiguration == null) {
- throw new InvalidOperationException("Could not create a valid cache configuration");
- }
- }
-
- return _cacheClientConfiguration;
- }
- }
-
- public IDatabase Cache {
- get {
-
- return GetConnection().GetDatabase();
- }
- }
-
- private ConnectionMultiplexer GetConnection() {
- var connectionMultiplexer = _connectionMultiplexers.GetOrAdd(CacheConfiguration, cfg => {
- Logger.Debug("Creating a new cache client ({0})", CacheConfiguration.GetHashCode());
- var connectionString = cfg.HostIdentifier + ",password=" + cfg.AuthorizationToken;
- return ConnectionMultiplexer.Connect(connectionString);
- });
-
-
- return connectionMultiplexer;
- }
-
- public void Set(string key, CacheItem cacheItem) {
- if (cacheItem.ValidFor <= 0) {
- return;
- }
-
- var value = JsonConvert.SerializeObject(cacheItem);
- Cache.StringSet(GetLocalizedKey(key), value, TimeSpan.FromSeconds(cacheItem.ValidFor));
- }
-
- public void Remove(string key) {
- Cache.KeyDelete(GetLocalizedKey(key));
- }
-
- public void RemoveAll() {
- foreach (var key in GetAllKeys()) {
- Remove(key);
- }
- }
-
- public CacheItem GetCacheItem(string key) {
- string value = Cache.StringGet(GetLocalizedKey(key));
- if(String.IsNullOrEmpty(value)) {
- return null;
- }
-
- return JsonConvert.DeserializeObject(value);
- }
-
- public IEnumerable GetCacheItems(int skip, int count) {
- foreach (var key in GetAllKeys().Skip(skip).Take(count)) {
- var cacheItem = GetCacheItem(key);
- // the item could have expired in the meantime
- if (cacheItem != null) {
- yield return cacheItem;
- }
- }
- }
-
- public int GetCacheItemsCount() {
- return GetAllKeys().Count();
- }
-
- ///
- /// Creates a namespaced key to support multiple tenants on top of a single Redis connection.
- ///
- /// The key to localized.
- /// A localized key based on the tenant name.
- private string GetLocalizedKey(string key) {
- return _shellSettings.Name + ":" + key;
- }
-
- ///
- /// Returns all the keys for the current tenant.
- ///
- /// The keys for the current tenant.
- private IEnumerable GetAllKeys() {
- // prevent the same request from computing the list twice (count + list)
- if (_keysCache == null) {
- _keysCache = new HashSet();
- var prefix = GetLocalizedKey("");
-
- var connection = GetConnection();
- foreach (var endPoint in connection.GetEndPoints()) {
- var server = GetConnection().GetServer(endPoint);
- foreach (var key in server.Keys(pattern: GetLocalizedKey("*"))) {
- _keysCache.Add(key.ToString().Substring(prefix.Length));
- }
- }
- }
-
- return _keysCache;
- }
- }
-}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheService.cs b/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheService.cs
index 378f576b3..af53e4b65 100644
--- a/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheService.cs
+++ b/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheService.cs
@@ -17,15 +17,15 @@ namespace Orchard.Caching.Services {
_prefix = shellSettings.Name;
}
- public object Get(string key) {
- return _cacheStorageProvider.Get(BuildFullKey(key));
+ public T Get(string key) {
+ return _cacheStorageProvider.Get(BuildFullKey(key));
}
- public void Put(string key, object value) {
+ public void Put(string key, T value) {
_cacheStorageProvider.Put(BuildFullKey(key), value);
}
- public void Put(string key, object value, TimeSpan validFor) {
+ public void Put(string key, T value, TimeSpan validFor) {
_cacheStorageProvider.Put(BuildFullKey(key), value, validFor);
}
@@ -38,7 +38,7 @@ namespace Orchard.Caching.Services {
}
private string BuildFullKey(string key) {
- return String.Concat(_prefix, "_", key);
+ return String.Concat(_prefix, ":", key);
}
}
}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheStorageProvider.cs
index ff8c31c37..c90e6e49d 100644
--- a/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheStorageProvider.cs
+++ b/src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheStorageProvider.cs
@@ -1,9 +1,6 @@
using System;
-using System.Collections;
using System.Globalization;
-using System.Linq;
using System.Runtime.Caching;
-using System.Web;
using Orchard.Environment.Configuration;
using Orchard.Services;
@@ -23,12 +20,12 @@ namespace Orchard.Caching.Services {
_clock = clock;
}
- public void Put(string key, object value) {
+ public void Put(string key, T value) {
// Keys are already prefixed by DefaultCacheService so no need to do it here again.
_cache.Set(key, value, GetCacheItemPolicy(MemoryCache.InfiniteAbsoluteExpiration));
}
- public void Put(string key, object value, TimeSpan validFor) {
+ public void Put(string key, T value, TimeSpan validFor) {
_cache.Set(key, value, GetCacheItemPolicy(new DateTimeOffset(_clock.UtcNow).ToOffset(validFor)));
}
@@ -42,8 +39,14 @@ namespace Orchard.Caching.Services {
}
}
- public object Get(string key) {
- return _cache.Get(key);
+ public T Get(string key) {
+ var value = _cache.Get(key) ;
+
+ if(value is T) {
+ return (T)value;
+ }
+
+ return default(T);
}
private CacheItemPolicy GetCacheItemPolicy(DateTimeOffset absoluteExpiration) {
diff --git a/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheService.cs b/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheService.cs
index 74ee9d39a..03aeede41 100644
--- a/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheService.cs
+++ b/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheService.cs
@@ -2,23 +2,18 @@
namespace Orchard.Caching.Services {
public interface ICacheService : IDependency {
+ T Get(string key);
- object Get(string key);
- void Put(string key, object value);
- void Put(string key, object value, TimeSpan validFor);
+ void Put(string key, T value);
+ void Put(string key, T value, TimeSpan validFor);
void Remove(string key);
void Clear();
}
public static class CachingExtensions {
-
- public static T Get(this ICacheService cacheService, string key) {
- return (T)cacheService.Get(key);
- }
-
public static T Get(this ICacheService cacheService, string key, Func factory) {
- var result = cacheService.Get(key);
+ var result = cacheService.Get(key);
if (result == null) {
var computed = factory();
cacheService.Put(key, computed);
@@ -26,11 +21,11 @@ namespace Orchard.Caching.Services {
}
// try to convert to T
- return (T)result;
+ return result;
}
public static T Get(this ICacheService cacheService, string key, Func factory, TimeSpan validFor) {
- var result = cacheService.Get(key);
+ var result = cacheService.Get(key);
if (result == null) {
var computed = factory();
cacheService.Put(key, computed, validFor);
@@ -38,7 +33,7 @@ namespace Orchard.Caching.Services {
}
// try to convert to T
- return (T)result;
+ return result;
}
}
}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheStorageProvider.cs
index 545b4f0df..60253600f 100644
--- a/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheStorageProvider.cs
+++ b/src/Orchard.Web/Modules/Orchard.Caching/Services/ICacheStorageProvider.cs
@@ -2,9 +2,9 @@
namespace Orchard.Caching.Services {
public interface ICacheStorageProvider : IDependency {
- object Get(string key);
- void Put(string key, object value);
- void Put(string key, object value, TimeSpan validFor);
+ T Get(string key);
+ void Put(string key, T value);
+ void Put(string key, T value, TimeSpan validFor);
void Remove(string key);
void Clear();
}
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/SqlServerBroker.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/SqlServerBroker.cs
new file mode 100644
index 000000000..6c4b00950
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/SqlServerBroker.cs
@@ -0,0 +1,126 @@
+using System;
+using System.Collections.Generic;
+using System.Data.SqlClient;
+using System.Linq;
+using System.Security.Permissions;
+using System.Threading.Tasks;
+using System.Web;
+using Orchard.Data;
+using Orchard.Environment;
+using Orchard.Environment.Configuration;
+using Orchard.Environment.Extensions;
+using Orchard.Logging;
+using Orchard.MessageBus.Models;
+using Orchard.MessageBus.Services;
+using Orchard.Services;
+
+namespace Orchard.MessageBus.Brokers.SqlServer {
+ ///
+ /// A single connection is maintained, and each subscription will be triggered based on the channel it's listening to
+ ///
+ [OrchardFeature("Orchard.MessageBus.SqlServerServiceBroker")]
+ public class SqlServerBroker : IMessageBroker, IDisposable {
+
+ private IWorker _worker;
+ private bool _initialized;
+ private object _synLock = new object();
+
+ private readonly Work> _messageRecordRepository;
+ private readonly Work _clock;
+ private readonly Func _workerFactory;
+ private readonly ShellSettings _shellSettings;
+ private readonly Work _hostNameProvider;
+
+ public SqlServerBroker(
+ Work> messageRecordRepository,
+ Work clock,
+ Work hostNameProvider,
+ Func workerFactory,
+ ShellSettings shellSettings
+ ) {
+ _messageRecordRepository = messageRecordRepository;
+ _clock = clock;
+ _shellSettings = shellSettings;
+ _workerFactory = workerFactory;
+ _hostNameProvider = hostNameProvider;
+
+ Logger = NullLogger.Instance;
+ }
+
+ public ILogger Logger { get; set; }
+
+ public bool EnsureInitialized() {
+ lock (_synLock) {
+ if (!_initialized) {
+ try {
+ // call only once per connectionstring when appdomain starts up
+ Logger.Information("Starting SqlDependency.");
+ SqlDependency.Start(_shellSettings.DataConnectionString);
+
+ _worker = _workerFactory();
+ _worker.Work();
+
+ _initialized = true;
+ }
+ catch (Exception e) {
+ Logger.Error("The application doesn't have the permission to request notifications.", e);
+ }
+ }
+
+ return _initialized;
+ }
+ }
+
+ public void Subscribe(string channel, Action handler) {
+ if (!EnsureInitialized()) {
+ return;
+ }
+
+ try {
+ lock (_synLock) {
+ _worker.RegisterHandler(channel, handler);
+ }
+ }
+ catch(Exception e) {
+ Logger.Error("An error occured while creating a Worker.", e);
+ }
+ }
+
+ public void Publish(string channel, string message) {
+ if (!EnsureInitialized()) {
+ return;
+ }
+
+ // clear old messages on publish to get a single worker
+ var oldMessages = _messageRecordRepository.Value
+ .Table
+ .Where(x => x.CreatedUtc <= _clock.Value.UtcNow.AddHours(-1))
+ .ToList();
+
+ foreach (var messageRecord in oldMessages) {
+ _messageRecordRepository.Value.Delete(messageRecord);
+ }
+
+ _messageRecordRepository.Value.Create(
+ new MessageRecord {
+ Channel = channel,
+ Message = message,
+ Publisher = _hostNameProvider.Value.GetHostName(),
+ CreatedUtc = _clock.Value.UtcNow
+ }
+ );
+ }
+
+ public void Dispose() {
+ // call only once per connectionstring when appdomain shuts down
+ if (!String.IsNullOrWhiteSpace(_shellSettings.DataConnectionString)) {
+ SqlDependency.Stop(_shellSettings.DataConnectionString);
+ }
+ }
+
+ private string GetHostName() {
+ // use the current host and the process id as two servers could run on the same machine
+ return System.Net.Dns.GetHostName() + ":" + System.Diagnostics.Process.GetCurrentProcess().Id;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs
new file mode 100644
index 000000000..cc6ab616e
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Brokers/SqlServer/Worker.cs
@@ -0,0 +1,200 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.SqlClient;
+using System.Linq;
+using System.Threading;
+using System.Web;
+using System.Web.Hosting;
+using Orchard.Environment.Configuration;
+using Orchard.Environment.Extensions;
+using Orchard.Logging;
+using Orchard.MessageBus.Models;
+using Orchard.MessageBus.Services;
+
+namespace Orchard.MessageBus.Brokers.SqlServer {
+ public interface IWorker : IDependency {
+ void Work();
+ void RegisterHandler(string channel, Action handler);
+ }
+
+ [OrchardFeature("Orchard.MessageBus.SqlServerServiceBroker")]
+ public class Worker : IWorker, IRegisteredObject {
+
+ private readonly ShellSettings _shellSettings;
+ private readonly IHostNameProvider _hostNameProvider;
+
+ private SqlDependency _dependency;
+
+ private static string commandText = "SELECT Id, Channel, Publisher, Message, CreatedUtc FROM dbo.{0}Orchard_MessageBus_MessageRecord WHERE Id > @Id";
+ private static int lastMessageId = 0;
+ private bool _stopped;
+
+ private Dictionary>> _handlers = new Dictionary>>();
+
+ public Worker(ShellSettings shellSettings, IHostNameProvider hostNameProvider) {
+ _hostNameProvider = hostNameProvider;
+ _shellSettings = shellSettings;
+
+ var tablePrefix = _shellSettings.DataTablePrefix;
+ if (!String.IsNullOrWhiteSpace(tablePrefix)) {
+ tablePrefix += "_";
+ }
+
+ commandText = String.Format(commandText, tablePrefix);
+
+ Logger = NullLogger.Instance;
+ }
+
+ public ILogger Logger { get; set; }
+
+ public void Work() {
+ // exit loop if stop notification as been triggered
+ if (_stopped) {
+ return;
+ }
+
+ try {
+ IEnumerable messages;
+
+ // load and process existing messages
+ using (var connection = new SqlConnection(_shellSettings.DataConnectionString)) {
+ connection.Open();
+
+ var command = CreateCommand(connection);
+ messages = GetMessages(command);
+ }
+
+ ProcessMessages(messages);
+
+ // wait for new messages to be available
+ WaitForWork();
+
+ }
+ catch (Exception e) {
+ Logger.Error("An unexpected error occured while monitoring sql dependencies.", e);
+ }
+ }
+
+ private void DoWork(object sender, SqlNotificationEventArgs eventArgs) {
+ Work();
+ }
+
+ private void WaitForWork() {
+
+ using (var connection = new SqlConnection(_shellSettings.DataConnectionString)) {
+ connection.Open();
+
+ using (var command = CreateCommand(connection)) {
+
+ // create a sql depdendency on the table we are monitoring
+ _dependency = new SqlDependency(command);
+
+ // when new records are present, continue the thread
+ _dependency.OnChange += DoWork;
+
+ // start monitoring the table
+ command.ExecuteNonQuery();
+ }
+ }
+ }
+
+ private void ProcessMessages(IEnumerable messages) {
+
+ // if this is the first time it's executed we just need to get the highest Id
+ if (lastMessageId == 0) {
+ lastMessageId = messages.Max(x => x.Id);
+ return;
+ }
+
+ // process the messages synchronously and in order of publication
+ foreach (var message in messages.OrderBy(x => x.Id)) {
+
+ // save the latest message id so that next time the table is monitored
+ // we get notified for new messages
+ lastMessageId = message.Id;
+
+ // only process handlers registered for the specific channel
+ List> channelHandlers;
+ if (_handlers.TryGetValue(message.Channel, out channelHandlers)) {
+
+ var hostName = _hostNameProvider.GetHostName();
+
+ // execute subscription
+ foreach (var handler in channelHandlers) {
+
+ // ignore messages sent by the current host
+ if (!message.Publisher.Equals(hostName, StringComparison.OrdinalIgnoreCase)) {
+ handler(message.Channel, message.Message);
+ }
+
+ // stop processing other messages if stop has been required
+ if (_stopped) {
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ public void Stop(bool immediate) {
+ _stopped = true;
+ }
+
+ public void RegisterHandler(string channel, Action handler) {
+ GetHandlersForChannel(channel).Add(handler);
+ }
+
+ private List> GetHandlersForChannel(string channel) {
+ List> channelHandlers;
+
+ if(!_handlers.TryGetValue(channel, out channelHandlers)) {
+ channelHandlers = new List>();
+ _handlers.Add(channel, channelHandlers);
+ }
+
+ return channelHandlers;
+ }
+
+ public SqlCommand CreateCommand(SqlConnection connection) {
+ SqlCommand command = new SqlCommand(commandText, connection);
+
+ SqlParameter param = new SqlParameter("@Id", SqlDbType.Int);
+ param.Direction = ParameterDirection.Input;
+ param.DbType = DbType.Int32;
+ param.Value = lastMessageId;
+ command.Parameters.Add(param);
+
+ return command;
+ }
+
+ public IEnumerable GetMessages(SqlCommand command) {
+ var result = new List();
+
+ try {
+
+ using (var reader = command.ExecuteReader()) {
+ if (reader.HasRows) {
+ while (reader.Read()) {
+ result.Add(new MessageRecord {
+ Id = reader.GetInt32(0),
+ Channel = reader.GetString(1),
+ Publisher = reader.GetString(2),
+ Message = reader.GetString(3),
+ CreatedUtc = reader.GetDateTime(4)
+ });
+ }
+ }
+ }
+ }
+ catch (Exception e) {
+ Logger.Error("Could not retreive Sql Broker messages.", e);
+ return Enumerable.Empty();
+ }
+
+ return result;
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Handler/MessageBusHandler.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Handler/MessageBusHandler.cs
new file mode 100644
index 000000000..296350c8d
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Handler/MessageBusHandler.cs
@@ -0,0 +1,9 @@
+using System.Linq;
+using JetBrains.Annotations;
+using Orchard.Data;
+using Orchard.ContentManagement.Handlers;
+
+namespace Orchard.MessageBus.Handlers {
+ public class MessageBusHandler : ContentHandler {
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Models/MessageRecord.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Models/MessageRecord.cs
new file mode 100644
index 000000000..bd6924662
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Models/MessageRecord.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using Orchard.Data.Conventions;
+using Orchard.Environment.Extensions;
+
+namespace Orchard.MessageBus.Models {
+
+ [OrchardFeature("Orchard.MessageBus.SqlServerServiceBroker")]
+ public class MessageRecord {
+
+ public virtual int Id { get; set; }
+ public virtual string Publisher { get; set; }
+ public virtual string Channel { get; set; }
+ public virtual DateTime CreatedUtc { get; set; }
+
+ [StringLengthMax]
+ public virtual string Message { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Module.txt b/src/Orchard.Web/Modules/Orchard.MessageBus/Module.txt
new file mode 100644
index 000000000..96dfd8582
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Module.txt
@@ -0,0 +1,27 @@
+Name: Orchard.MessageBus
+AntiForgery: enabled
+Author: The Orchard Team
+Website: http://orchardproject.net
+Version: 1.0
+OrchardVersion: 1.0
+Description: Provides communication APIs for server farms.
+Features:
+ Orchard.MessageBus:
+ Name: Message Bus
+ Description: Reusable API abstractions to communicate in a server farm.
+ Category: Hosting
+ Orchard.MessageBus.DistributedSignals:
+ Name: Distributed Signals
+ Description: Distribute signals cache invalidation calls.
+ Dependencies: Orchard.MessageBus
+ Category: Hosting
+ Orchard.MessageBus.SqlServerServiceBroker:
+ Name: SQL Server Service Broker
+ Description: A message bus implementation using SQL Server Service Broker.
+ Dependencies: Orchard.MessageBus
+ Category: Hosting
+ Orchard.MessageBus.DistributedShellRestart
+ Name: Distributed Shell Restart
+ Description: Distribute shell restarts.
+ Dependencies: Orchard.MessageBus
+ Category: Hosting
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Orchard.MessageBus.csproj b/src/Orchard.Web/Modules/Orchard.MessageBus/Orchard.MessageBus.csproj
new file mode 100644
index 000000000..600bf4d12
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Orchard.MessageBus.csproj
@@ -0,0 +1,154 @@
+
+
+
+
+ Debug
+ AnyCPU
+ 9.0.30729
+ 2.0
+ {ED715544-E649-4F48-B8EE-9368C41C3AC0}
+ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
+ Library
+ Properties
+ Orchard.MessageBus
+ Orchard.MessageBus
+ v4.5.1
+ false
+
+
+ 4.0
+
+
+ false
+
+
+
+
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+ AllRules.ruleset
+ false
+
+
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ AllRules.ruleset
+ false
+
+
+
+
+ False
+ ..\..\..\..\lib\nhibernate\NHibernate.dll
+
+
+
+
+ 3.5
+
+
+
+ False
+ ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}
+ Orchard.Framework
+
+
+ {9916839C-39FC-4CEB-A5AF-89CA7E87119F}
+ Orchard.Core
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+
+
+
+ $(ProjectDir)\..\Manifests
+
+
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 45979
+ /
+
+
+ False
+ True
+ http://orchard.codeplex.com
+ False
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..220f9c927
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Properties/AssemblyInfo.cs
@@ -0,0 +1,37 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Orchard.MessageBus")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyProduct("Orchard")]
+[assembly: AssemblyCopyright("")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("5b79fe08-1e5b-44ab-9323-2c08a7e461e5")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Scripts/Web.config b/src/Orchard.Web/Modules/Orchard.MessageBus/Scripts/Web.config
new file mode 100644
index 000000000..817198995
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Scripts/Web.config
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs
new file mode 100644
index 000000000..e38098db8
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DefaultMessageBus.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Web;
+using System.Threading.Tasks;
+using Orchard.Logging;
+
+namespace Orchard.MessageBus.Services {
+ public class DefaultMessageBus : IMessageBus {
+ private readonly IMessageBroker _messageBroker;
+
+ public DefaultMessageBus(IEnumerable messageBrokers) {
+ _messageBroker = messageBrokers.FirstOrDefault();
+
+ Logger = NullLogger.Instance;
+ }
+
+ public ILogger Logger { get; set; }
+
+ public void Subscribe(string channel, Action handler) {
+ if (_messageBroker == null) {
+ return;
+ }
+
+ _messageBroker.Subscribe(channel, handler);
+ Logger.Debug("{0} subscribed to {1}", GetHostName(), channel);
+ }
+
+ public void Publish(string channel, string message) {
+ if (_messageBroker == null) {
+ return;
+ }
+
+ _messageBroker.Publish(channel, message);
+ Logger.Debug("{0} published {1}", GetHostName(), channel);
+ }
+
+ private string GetHostName() {
+ // use the current host and the process id as two servers could run on the same machine
+ return System.Net.Dns.GetHostName() + ":" + System.Diagnostics.Process.GetCurrentProcess().Id;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellStarter.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellStarter.cs
new file mode 100644
index 000000000..97bef5f20
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellStarter.cs
@@ -0,0 +1,51 @@
+using System.Linq;
+using Orchard.Caching;
+using Orchard.Environment;
+using Orchard.Environment.Configuration;
+using Orchard.Environment.Extensions;
+
+namespace Orchard.MessageBus.Services {
+
+ public interface IDistributedShellStarter : ISingletonDependency {
+
+ }
+
+ [OrchardFeature("Orchard.MessageBus.DistributedShellRestart")]
+ public class DistributedShellStarter : IDistributedShellStarter, IOrchardShellEvents {
+ private readonly IWorkContextAccessor _workContextAccessor;
+
+ private readonly IMessageBus _messageBus;
+
+ public readonly static string Channel = "ShellChanged";
+
+ public DistributedShellStarter(IMessageBus messageBus, IWorkContextAccessor workContextAccessor) {
+ _messageBus = messageBus;
+ _workContextAccessor = workContextAccessor;
+ }
+
+ public void Activated() {
+ _messageBus.Subscribe(Channel, (channel, message) => {
+ // todo: this only handles changed tenants, we should consider handling started and stopped tenants
+
+ using (var scope = _workContextAccessor.CreateWorkContextScope()) {
+ var shellSettings = scope.Resolve();
+ if (shellSettings != null) {
+
+ // todo: this doesn't work as the new tenants list is lost right after
+ var shellSettingsManagerEventHandler = scope.Resolve();
+ shellSettingsManagerEventHandler.Saved(shellSettings);
+
+ var orchardHost = scope.Resolve() as DefaultOrchardHost;
+ if(orchardHost != null) {
+ var startUpdatedShellsMethod = typeof(DefaultOrchardHost).GetMethod("StartUpdatedShells", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ startUpdatedShellsMethod.Invoke(orchardHost, null);
+ }
+ }
+ }
+ });
+ }
+
+ public void Terminating() {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellTrigger.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellTrigger.cs
new file mode 100644
index 000000000..d5e888fe0
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedShellTrigger.cs
@@ -0,0 +1,24 @@
+using Orchard.Environment.Configuration;
+using Orchard.Environment.Descriptor;
+using Orchard.Environment.Descriptor.Models;
+using Orchard.Environment.Extensions;
+
+namespace Orchard.MessageBus.Services {
+ [OrchardFeature("Orchard.MessageBus.DistributedShellRestart")]
+ public class DistributedShellTrigger : IShellDescriptorManagerEventHandler, IShellSettingsManagerEventHandler {
+
+ private readonly IMessageBus _messageBus;
+
+ public DistributedShellTrigger(IShellSettingsManager shellSettingsManager, IMessageBus messageBus, IShellSettingsManagerEventHandler shellSettingsManagerEventHandler) {
+ _messageBus = messageBus;
+ }
+
+ void IShellDescriptorManagerEventHandler.Changed(ShellDescriptor descriptor, string tenant) {
+ _messageBus.Publish(DistributedShellStarter.Channel, tenant);
+ }
+
+ void IShellSettingsManagerEventHandler.Saved(ShellSettings settings) {
+ _messageBus.Publish(DistributedShellStarter.Channel, settings.Name);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedSignals.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedSignals.cs
new file mode 100644
index 000000000..c24661d5a
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/DistributedSignals.cs
@@ -0,0 +1,35 @@
+using Orchard.Caching;
+using Orchard.Environment;
+using Orchard.Environment.Extensions;
+using Orchard.MessageBus.Services;
+using Orchard.Services;
+
+namespace Orchard.MessageBus.Services {
+ [OrchardFeature("Orchard.MessageBus.DistributedSignals")]
+ [OrchardSuppressDependency("Orchard.Caching.Signals")]
+ public class DistributedSignals : Signals, ISignals, IOrchardShellEvents {
+ private readonly IMessageBus _messageBus;
+
+ public DistributedSignals(IMessageBus messageBus) {
+ _messageBus = messageBus;
+ }
+
+ void ISignals.Trigger(T signal) {
+ base.Trigger(signal);
+ _messageBus.Publish("Signal", signal.ToString());
+ }
+
+ IVolatileToken ISignals.When(T signal) {
+ return base.When(signal);
+ }
+
+ public void Activated() {
+ _messageBus.Subscribe("Signal", (channel, message) => {
+ base.Trigger(message);
+ });
+ }
+
+ public void Terminating() {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/HostNameProvider.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/HostNameProvider.cs
new file mode 100644
index 000000000..af4aeb48c
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/HostNameProvider.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Orchard.MessageBus.Services {
+ public class HostNameProvider : IHostNameProvider {
+
+ public string GetHostName() {
+ // use the current host and the process id as two servers could run on the same machine
+ return System.Net.Dns.GetHostName() + ":" + System.Diagnostics.Process.GetCurrentProcess().Id;
+ }
+ }
+}
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IHostNameProvider.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IHostNameProvider.cs
new file mode 100644
index 000000000..5e1583c0b
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IHostNameProvider.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Orchard.MessageBus.Services {
+ public interface IHostNameProvider : IDependency {
+ string GetHostName();
+ }
+}
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBroker.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBroker.cs
new file mode 100644
index 000000000..d7d5a1711
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBroker.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Orchard.MessageBus.Services {
+ public interface IMessageBroker : ISingletonDependency {
+ void Subscribe(string channel, Action handler);
+ void Publish(string channel, string message);
+ }
+}
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBus.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBus.cs
new file mode 100644
index 000000000..3e691babf
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/IMessageBus.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Orchard.MessageBus.Models;
+
+namespace Orchard.MessageBus.Services {
+ public interface IMessageBus : ISingletonDependency {
+ void Subscribe(string channel, Action handler);
+ void Publish(string channel, string message);
+ }
+}
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Services/MessageBusNotificationProvider.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/MessageBusNotificationProvider.cs
new file mode 100644
index 000000000..715e69a4d
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Services/MessageBusNotificationProvider.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using Orchard.Localization;
+using Orchard.UI.Admin.Notification;
+using Orchard.UI.Notify;
+using System.Linq;
+
+namespace Orchard.MessageBus.Services {
+ public class MessageBusNotificationProvider : INotificationProvider {
+ private readonly IEnumerable _messageBrokers;
+
+ public MessageBusNotificationProvider(IEnumerable messageBrokers) {
+ _messageBrokers = messageBrokers;
+ T = NullLocalizer.Instance;
+ }
+
+ public Localizer T { get; set; }
+
+ public IEnumerable GetNotifications() {
+
+ if (!_messageBrokers.Any()) {
+ yield return new NotifyEntry { Message = T("You need to enable an message bus broker implementation like SQL Server Service Broker."), Type = NotifyType.Warning };
+ }
+ }
+ }
+}
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/SqlServerBrokerMigrations.cs b/src/Orchard.Web/Modules/Orchard.MessageBus/SqlServerBrokerMigrations.cs
new file mode 100644
index 000000000..cc29857c2
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/SqlServerBrokerMigrations.cs
@@ -0,0 +1,25 @@
+using System;
+using Orchard.ContentManagement.MetaData;
+using Orchard.Data.Migration;
+using Orchard.Environment.Extensions;
+using Orchard.MessageBus.Models;
+
+namespace Orchard.MessageBus {
+ [OrchardFeature("Orchard.MessageBus.SqlServerServiceBroker")]
+ public class SqlServerBrokerMigrations : DataMigrationImpl {
+
+ public int Create() {
+
+ SchemaBuilder.CreateTable("MessageRecord",
+ table => table
+ .Column("Id", c => c.PrimaryKey().Identity())
+ .Column("Publisher", c => c.WithLength(255))
+ .Column("Channel", c => c.WithLength(255))
+ .Column("Message", c => c.Unlimited())
+ .Column("CreatedUtc")
+ );
+
+ return 1;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Styles/Web.config b/src/Orchard.Web/Modules/Orchard.MessageBus/Styles/Web.config
new file mode 100644
index 000000000..817198995
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Styles/Web.config
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Orchard.Web/Modules/Orchard.MessageBus/Web.config b/src/Orchard.Web/Modules/Orchard.MessageBus/Web.config
new file mode 100644
index 000000000..6fc84bc30
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.MessageBus/Web.config
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Orchard.Web/Modules/Orchard.OutputCache/Module.txt b/src/Orchard.Web/Modules/Orchard.OutputCache/Module.txt
index e20077aff..30e696b98 100644
--- a/src/Orchard.Web/Modules/Orchard.OutputCache/Module.txt
+++ b/src/Orchard.Web/Modules/Orchard.OutputCache/Module.txt
@@ -12,4 +12,4 @@ Features:
Orchard.OutputCache.Database:
Description: Stores output cache data in the database.
Category: Performance
- Dependencies: Orchard.OutputCache
+ Dependencies: Orchard.OutputCache
diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs
new file mode 100644
index 000000000..edbe6ced3
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.Redis/Caching/RedisCacheStorageProvider.cs
@@ -0,0 +1,62 @@
+using Newtonsoft.Json;
+using Orchard.Caching.Services;
+using Orchard.Environment.Configuration;
+using Orchard.Environment.Extensions;
+using Orchard.Logging;
+using Orchard.Redis.Configuration;
+using Orchard.Redis.Extensions;
+using StackExchange.Redis;
+using System;
+
+namespace Orchard.Redis.Caching {
+
+ [OrchardFeature("Orchard.Redis.Caching")]
+ [OrchardSuppressDependency("Orchard.Caching.Services.DefaultCacheStorageProvider")]
+ public class RedisCacheStorageProvider : Component, ICacheStorageProvider {
+ public const string ConnectionStringKey = "Orchard.Redis.Cache";
+
+ private readonly ShellSettings _shellSettings;
+ private readonly IRedisConnectionProvider _redisConnectionProvider;
+ private readonly string _connectionString;
+
+ public IDatabase Database {
+ get {
+ return _redisConnectionProvider.GetConnection(_connectionString).GetDatabase();
+ }
+ }
+
+ public RedisCacheStorageProvider(ShellSettings shellSettings, IRedisConnectionProvider redisConnectionProvider) {
+ _shellSettings = shellSettings;
+ _redisConnectionProvider = redisConnectionProvider;
+ _connectionString = _redisConnectionProvider.GetConnectionString(ConnectionStringKey);
+ Logger = NullLogger.Instance;
+ }
+
+ public T Get(string key) {
+ var json = Database.StringGet(GetLocalizedKey(key));
+ return JsonConvert.DeserializeObject(json);
+ }
+
+ public void Put(string key, T value) {
+ var json = JsonConvert.SerializeObject(value);
+ Database.StringSet(GetLocalizedKey(key), json, null);
+ }
+
+ public void Put(string key, T value, TimeSpan validFor) {
+ var json = JsonConvert.SerializeObject(value);
+ Database.StringSet(GetLocalizedKey(key), json, validFor);
+ }
+
+ public void Remove(string key) {
+ Database.KeyDelete(key);
+ }
+
+ public void Clear() {
+ Database.KeyDeleteWithPrefix(GetLocalizedKey("*"));
+ }
+
+ private string GetLocalizedKey(string key) {
+ return _shellSettings.Name + ":Cache:" + key;
+ }
+ }
+}
diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs
new file mode 100644
index 000000000..0139191b8
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/IRedisConnectionProvider.cs
@@ -0,0 +1,10 @@
+using StackExchange.Redis;
+
+namespace Orchard.Redis.Configuration {
+
+ public interface IRedisConnectionProvider : ISingletonDependency {
+ ConnectionMultiplexer GetConnection(string connectionString);
+ string GetConnectionString(string service);
+ }
+
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs
new file mode 100644
index 000000000..e3e994e5a
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.Redis/Configuration/RedisConnectionProvider.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Concurrent;
+using System.Configuration;
+using Orchard.Environment.Configuration;
+using Orchard.Logging;
+using StackExchange.Redis;
+
+namespace Orchard.Redis.Configuration {
+
+ public class RedisConnectionProvider : IRedisConnectionProvider {
+ private static ConcurrentDictionary _connectionMultiplexers = new ConcurrentDictionary();
+ private readonly ShellSettings _shellSettings;
+
+ public RedisConnectionProvider(ShellSettings shellSettings) {
+ _shellSettings = shellSettings;
+ Logger = NullLogger.Instance;
+ }
+
+ public ILogger Logger { get; set; }
+
+ public string GetConnectionString(string service) {
+ var _tenantSettingsKey = _shellSettings.Name + ":" + service;
+ var _defaultSettingsKey = service;
+
+ var connectionStringSettings = ConfigurationManager.ConnectionStrings[_tenantSettingsKey] ?? ConfigurationManager.ConnectionStrings[_defaultSettingsKey];
+
+ if (connectionStringSettings == null) {
+ throw new ConfigurationErrorsException("A connection string is expected for " + service);
+ }
+
+ return connectionStringSettings.ConnectionString;
+ }
+
+ public ConnectionMultiplexer GetConnection(string connectionString) {
+
+ if (String.IsNullOrWhiteSpace(connectionString)) {
+ throw new ArgumentNullException("connectionString");
+ }
+
+ var connectionMultiplexer = _connectionMultiplexers.GetOrAdd(connectionString, cfg => {
+ Logger.Debug("Creating a new cache client for: {0}", connectionString);
+ return ConnectionMultiplexer.Connect(connectionString);
+ });
+
+ return connectionMultiplexer;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs b/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs
new file mode 100644
index 000000000..2b223a064
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.Redis/Extensions/RedisDatabaseExtensions.cs
@@ -0,0 +1,41 @@
+using System;
+using StackExchange.Redis;
+
+namespace Orchard.Redis.Extensions {
+ public static class RedisDatabaseExtensions {
+
+ public static void KeyDeleteWithPrefix(this IDatabase database, string prefix) {
+ if (database == null) {
+ throw new ArgumentException("Database cannot be null", "database");
+ }
+
+ if (string.IsNullOrWhiteSpace(prefix)) {
+ throw new ArgumentException("Prefix cannot be empty", "database");
+ }
+
+ database.ScriptEvaluate(@"
+ local keys = redis.call('keys', ARGV[1])
+ for i=1,#keys,5000 do
+ redis.call('del', unpack(keys, i, math.min(i+4999, #keys)))
+ end", values: new RedisValue[] { prefix });
+ }
+
+ public static int KeyCount(this IDatabase database, string prefix) {
+ if (database == null) {
+ throw new ArgumentException("Database cannot be null", "database");
+ }
+
+ if (string.IsNullOrWhiteSpace(prefix)) {
+ throw new ArgumentException("Prefix cannot be empty", "database");
+ }
+
+ var retVal = database.ScriptEvaluate("return table.getn(redis.call('keys', ARGV[1]))", values: new RedisValue[] { prefix });
+
+ if (retVal.IsNull) {
+ return 0;
+ }
+
+ return (int)retVal;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.Redis/MessageBus/RedisMessageBusBroker.cs b/src/Orchard.Web/Modules/Orchard.Redis/MessageBus/RedisMessageBusBroker.cs
new file mode 100644
index 000000000..90b1d7d09
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.Redis/MessageBus/RedisMessageBusBroker.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using Orchard.Environment.Configuration;
+using Orchard.Environment.Extensions;
+using Orchard.Logging;
+using Orchard.MessageBus.Services;
+using Orchard.Redis.Configuration;
+using StackExchange.Redis;
+
+namespace Orchard.Redis.MessageBus {
+
+ [OrchardFeature("Orchard.Redis.MessageBus")]
+ public class RedisMessageBusBroker : Component, IMessageBroker {
+
+ private readonly IRedisConnectionProvider _redisConnectionProvider;
+
+ public const string ConnectionStringKey = "Orchard.Redis.MessageBus";
+ private readonly string _connectionString;
+
+ private ConcurrentDictionary>> _handlers = new ConcurrentDictionary>>();
+
+ public RedisMessageBusBroker(ShellSettings shellSettings, IRedisConnectionProvider redisConnectionProvider) {
+ _redisConnectionProvider = redisConnectionProvider;
+ _connectionString = _redisConnectionProvider.GetConnectionString(ConnectionStringKey);
+ }
+
+ public IDatabase Database {
+ get {
+ return _redisConnectionProvider.GetConnection(_connectionString).GetDatabase();
+ }
+ }
+
+ public void Subscribe(string channel, Action handler) {
+
+ try {
+ var channelHandlers = _handlers.GetOrAdd(channel, c => {
+ return new ConcurrentBag>();
+ });
+
+ channelHandlers.Add(handler);
+
+ var sub = _redisConnectionProvider.GetConnection(_connectionString).GetSubscriber();
+ sub.Subscribe(channel, (c, m) => {
+
+ // the message contains the publisher before the first '/'
+ var messageTokens = m.ToString().Split('/');
+ var publisher = messageTokens.FirstOrDefault();
+ var message = messageTokens.Skip(1).FirstOrDefault();
+
+ if (String.IsNullOrWhiteSpace(publisher)) {
+ return;
+ }
+
+ // ignore self sent messages
+ if (GetHostName().Equals(publisher, StringComparison.OrdinalIgnoreCase)) {
+ return;
+ }
+
+ Logger.Debug("Processing {0}", message);
+ handler(c, message);
+ });
+
+ }
+ catch (Exception e) {
+ Logger.Error("An error occured while subscribing to " + channel, e);
+ }
+ }
+
+ public void Publish(string channel, string message) {
+ Database.Publish(channel, GetHostName() + "/" + message);
+ }
+
+ private string GetHostName() {
+ // use the current host and the process id as two servers could run on the same machine
+ return System.Net.Dns.GetHostName() + ":" + System.Diagnostics.Process.GetCurrentProcess().Id;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Module.txt b/src/Orchard.Web/Modules/Orchard.Redis/Module.txt
new file mode 100644
index 000000000..86d7bf66f
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.Redis/Module.txt
@@ -0,0 +1,27 @@
+Name: Orchard.Redis
+AntiForgery: enabled
+Author: The Orchard Team
+Website: http://orchardproject.net
+Version: 1.0
+OrchardVersion: 1.0
+Description: Provides Redis integration with Orchard.
+Features:
+ Orchard.Redis
+ Name: Redis
+ Description: Used to provide Redis related functionalities.
+ Category: Hosting
+ Orchard.Redis.MessageBus:
+ Name: Redis Message Bus
+ Description: A message bus implementation using Redis pub/sub.
+ Category: Hosting
+ Dependencies: Orchard.MessageBus, Orchard.Redis
+ Orchard.Redis.OutputCache:
+ Name: Redis Output Cache
+ Description: An output cache storage provider using Redis.
+ Category: Performance
+ Dependencies: Orchard.OutputCache, Orchard.Redis
+ Orchard.Redis.Caching:
+ Name: Redis Cache
+ Description: Business data cache using Redis.
+ Category: Performance
+ Dependencies: Orchard.Caching, Orchard.Redis
diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj
new file mode 100644
index 000000000..4590be157
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj
@@ -0,0 +1,159 @@
+
+
+
+
+ Debug
+ AnyCPU
+ 9.0.30729
+ 2.0
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}
+ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
+ Library
+ Properties
+ Orchard.Redis
+ Orchard.Redis
+ v4.5.1
+ false
+
+
+ 4.0
+
+
+ false
+
+
+
+
+
+
+ true
+ full
+ false
+ bin\
+ DEBUG;TRACE
+ prompt
+ 4
+ AllRules.ruleset
+ false
+
+
+ pdbonly
+ true
+ bin\
+ TRACE
+ prompt
+ 4
+ AllRules.ruleset
+ false
+
+
+
+
+ ..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll
+
+
+ False
+ ..\..\..\..\lib\nhibernate\NHibernate.dll
+
+
+ ..\..\..\..\lib\redis\StackExchange.Redis.dll
+ True
+
+
+
+
+ 3.5
+
+
+
+ False
+ ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}
+ Orchard.Framework
+
+
+ {9916839C-39FC-4CEB-A5AF-89CA7E87119F}
+ Orchard.Core
+
+
+ {7528BF74-25C7-4ABE-883A-443B4EEC4776}
+ Orchard.Caching
+
+
+ {ed715544-e649-4f48-b8ee-9368c41c3ac0}
+ Orchard.MessageBus
+
+
+ {6e444ff1-a47c-4cf6-bb3f-507c8ebd776d}
+ Orchard.OutputCache
+
+
+
+
+
+
+
+
+
+
+
+ 10.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+
+
+
+ $(ProjectDir)\..\Manifests
+
+
+
+
+
+
+
+
+
+
+
+ False
+ True
+ 45979
+ /
+
+
+ False
+ True
+ http://orchard.codeplex.com
+ False
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs b/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs
new file mode 100644
index 000000000..85cde139e
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.Redis/OutputCache/RedisOutputCacheStorageProvider.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json;
+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;
+
+namespace Orchard.Redis.OutputCache {
+
+ [OrchardFeature("Orchard.Redis.OutputCache")]
+ [OrchardSuppressDependency("Orchard.OutputCache.Services.DefaultCacheStorageProvider")]
+ public class RedisOutputCacheStorageProvider : Component, IOutputCacheStorageProvider {
+
+ private readonly ShellSettings _shellSettings;
+ private readonly IRedisConnectionProvider _redisConnectionProvider;
+ private HashSet _keysCache;
+
+ public const string ConnectionStringKey = "Orchard.Redis.OutputCache";
+ private readonly string _connectionString;
+
+ public RedisOutputCacheStorageProvider(ShellSettings shellSettings, IRedisConnectionProvider redisConnectionProvider) {
+ _shellSettings = shellSettings;
+ _redisConnectionProvider = redisConnectionProvider;
+ _connectionString = _redisConnectionProvider.GetConnectionString(ConnectionStringKey);
+
+ Logger = NullLogger.Instance;
+ }
+
+ public IDatabase Database {
+ get {
+ return _redisConnectionProvider.GetConnection(_connectionString).GetDatabase();
+ }
+ }
+
+ public void Set(string key, CacheItem cacheItem) {
+ if (cacheItem.ValidFor <= 0) {
+ return;
+ }
+
+ var value = JsonConvert.SerializeObject(cacheItem);
+ Database.StringSet(GetLocalizedKey(key), value, TimeSpan.FromSeconds(cacheItem.ValidFor));
+ }
+
+ public void Remove(string key) {
+ Database.KeyDelete(GetLocalizedKey(key));
+ }
+
+ public void RemoveAll() {
+ Database.KeyDeleteWithPrefix(GetLocalizedKey("*"));
+ }
+
+ public CacheItem GetCacheItem(string key) {
+ string value = Database.StringGet(GetLocalizedKey(key));
+ if (String.IsNullOrEmpty(value)) {
+ return null;
+ }
+
+ return JsonConvert.DeserializeObject(value);
+ }
+
+ public IEnumerable GetCacheItems(int skip, int count) {
+ foreach (var key in GetAllKeys().Skip(skip).Take(count)) {
+ var cacheItem = GetCacheItem(key);
+ // the item could have expired in the meantime
+ if (cacheItem != null) {
+ yield return cacheItem;
+ }
+ }
+ }
+
+ public int GetCacheItemsCount() {
+ return Database.KeyCount(GetLocalizedKey("*"));
+ }
+
+ ///
+ /// Creates a namespaced key to support multiple tenants on top of a single Redis connection.
+ ///
+ /// The key to localized.
+ /// A localized key based on the tenant name.
+ private string GetLocalizedKey(string key) {
+ return _shellSettings.Name + ":OutputCache:" + key;
+ }
+
+ ///
+ /// Returns all the keys for the current tenant.
+ ///
+ /// The keys for the current tenant.
+ private IEnumerable GetAllKeys() {
+ // prevent the same request from computing the list twice (count + list)
+ if (_keysCache == null) {
+ _keysCache = new HashSet();
+ var prefix = GetLocalizedKey("");
+
+ var connection = _redisConnectionProvider.GetConnection(_connectionString);
+ foreach (var endPoint in connection.GetEndPoints()) {
+ var server = connection.GetServer(endPoint);
+ foreach (var key in server.Keys(pattern: GetLocalizedKey("*"))) {
+ _keysCache.Add(key.ToString().Substring(prefix.Length));
+ }
+ }
+ }
+
+ return _keysCache;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.Redis/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..fc426464d
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.Redis/Properties/AssemblyInfo.cs
@@ -0,0 +1,37 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Security;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Orchard.Redis")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyProduct("Orchard")]
+[assembly: AssemblyCopyright("")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("f4511dd5-71d3-438d-b5a2-42f0461833be")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
+
diff --git a/src/Orchard.Web/Modules/Orchard.Redis/Web.config b/src/Orchard.Web/Modules/Orchard.Redis/Web.config
new file mode 100644
index 000000000..6fc84bc30
--- /dev/null
+++ b/src/Orchard.Web/Modules/Orchard.Redis/Web.config
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Orchard.sln b/src/Orchard.sln
index 9426ecded..a88be2f7d 100644
--- a/src/Orchard.sln
+++ b/src/Orchard.sln
@@ -243,6 +243,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Azure.MediaServices
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.AuditTrail", "Orchard.Web\Modules\Orchard.AuditTrail\Orchard.AuditTrail.csproj", "{3DD574CD-9C5D-4A45-85E1-EBBA64C22B5F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.MessageBus", "Orchard.Web\Modules\Orchard.MessageBus\Orchard.MessageBus.csproj", "{ED715544-E649-4F48-B8EE-9368C41C3AC0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Redis", "Orchard.Web\Modules\Orchard.Redis\Orchard.Redis.csproj", "{2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Layouts", "Orchard.Web\Modules\Orchard.Layouts\Orchard.Layouts.csproj", "{6BD8B2FA-F2E3-4AC8-A4C3-2925A653889A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.DynamicForms", "Orchard.Web\Modules\Orchard.DynamicForms\Orchard.DynamicForms.csproj", "{82190F52-2901-46D6-8A4C-34649959483F}"
@@ -1032,6 +1036,23 @@ Global
{82190F52-2901-46D6-8A4C-34649959483F}.FxCop|Any CPU.Build.0 = Release|Any CPU
{82190F52-2901-46D6-8A4C-34649959483F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{82190F52-2901-46D6-8A4C-34649959483F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {ED715544-E649-4F48-B8EE-9368C41C3AC0}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
+ {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {ED715544-E649-4F48-B8EE-9368C41C3AC0}.FxCop|Any CPU.ActiveCfg = Release|Any CPU
+ {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {ED715544-E649-4F48-B8EE-9368C41C3AC0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.CodeCoverage|Any CPU.ActiveCfg = Release|Any CPU
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.CodeCoverage|Any CPU.Build.0 = Release|Any CPU
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Coverage|Any CPU.ActiveCfg = Release|Any CPU
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Coverage|Any CPU.Build.0 = Release|Any CPU
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.FxCop|Any CPU.ActiveCfg = Release|Any CPU
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.FxCop|Any CPU.Build.0 = Release|Any CPU
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1111,6 +1132,8 @@ Global
{7528BF74-25C7-4ABE-883A-443B4EEC4776} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{14A96B1A-9DC9-44C8-A675-206329E15263} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{3DD574CD-9C5D-4A45-85E1-EBBA64C22B5F} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
+ {ED715544-E649-4F48-B8EE-9368C41C3AC0} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
+ {2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{6BD8B2FA-F2E3-4AC8-A4C3-2925A653889A} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
{82190F52-2901-46D6-8A4C-34649959483F} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5}
EndGlobalSection