mirror of
https://github.com/OrchardCMS/Orchard.git
synced 2026-02-09 09:16:41 +08:00
Merge branch 'feature/messagebus' into 1.x
Conflicts: src/Orchard.Web/Modules/Orchard.Caching/Services/DefaultCacheStorageProvider.cs
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -93,9 +93,6 @@
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\nhibernate\NHibernate.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="StackExchange.Redis">
|
||||
<HintPath>..\..\..\..\lib\redis\StackExchange.Redis.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
@@ -111,7 +108,6 @@
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Services\Caching\Output\AzureRedisOutputCacheStorageProvider.cs" />
|
||||
<Compile Include="Services\Environment\Configuration\DefaultPlatformConfigurationAccessor.cs" />
|
||||
<Compile Include="Services\Environment\Configuration\IPlatformConfigurationAccessor.cs" />
|
||||
<Compile Include="Services\TaskLease\AzureMachineNameProvider.cs" />
|
||||
|
||||
@@ -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<CacheClientConfiguration, ConnectionMultiplexer> _connectionMultiplexers = new ConcurrentDictionary<CacheClientConfiguration, ConnectionMultiplexer>();
|
||||
|
||||
private readonly ICacheManager _cacheManager;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
private readonly IPlatformConfigurationAccessor _pca;
|
||||
private HashSet<string> _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<CacheItem>(value);
|
||||
}
|
||||
|
||||
public IEnumerable<CacheItem> 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();
|
||||
}
|
||||
|
||||
/// <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 + ":" + 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("");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,15 @@ namespace Orchard.Caching.Services {
|
||||
_prefix = shellSettings.Name;
|
||||
}
|
||||
|
||||
public object Get(string key) {
|
||||
return _cacheStorageProvider.Get(BuildFullKey(key));
|
||||
public T Get<T>(string key) {
|
||||
return _cacheStorageProvider.Get<T>(BuildFullKey(key));
|
||||
}
|
||||
|
||||
public void Put(string key, object value) {
|
||||
public void Put<T>(string key, T value) {
|
||||
_cacheStorageProvider.Put(BuildFullKey(key), value);
|
||||
}
|
||||
|
||||
public void Put(string key, object value, TimeSpan validFor) {
|
||||
public void Put<T>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T>(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<T>(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<T>(string key) {
|
||||
var value = _cache.Get(key) ;
|
||||
|
||||
if(value is T) {
|
||||
return (T)value;
|
||||
}
|
||||
|
||||
return default(T);
|
||||
}
|
||||
|
||||
private CacheItemPolicy GetCacheItemPolicy(DateTimeOffset absoluteExpiration) {
|
||||
|
||||
@@ -2,23 +2,18 @@
|
||||
|
||||
namespace Orchard.Caching.Services {
|
||||
public interface ICacheService : IDependency {
|
||||
T Get<T>(string key);
|
||||
|
||||
object Get(string key);
|
||||
void Put(string key, object value);
|
||||
void Put(string key, object value, TimeSpan validFor);
|
||||
void Put<T>(string key, T value);
|
||||
void Put<T>(string key, T value, TimeSpan validFor);
|
||||
|
||||
void Remove(string key);
|
||||
void Clear();
|
||||
}
|
||||
|
||||
public static class CachingExtensions {
|
||||
|
||||
public static T Get<T>(this ICacheService cacheService, string key) {
|
||||
return (T)cacheService.Get(key);
|
||||
}
|
||||
|
||||
public static T Get<T>(this ICacheService cacheService, string key, Func<T> factory) {
|
||||
var result = cacheService.Get(key);
|
||||
var result = cacheService.Get<T>(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<T>(this ICacheService cacheService, string key, Func<T> factory, TimeSpan validFor) {
|
||||
var result = cacheService.Get(key);
|
||||
var result = cacheService.Get<T>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T>(string key);
|
||||
void Put<T>(string key, T value);
|
||||
void Put<T>(string key, T value, TimeSpan validFor);
|
||||
void Remove(string key);
|
||||
void Clear();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
/// <summary>
|
||||
/// A single connection is maintained, and each subscription will be triggered based on the channel it's listening to
|
||||
/// </summary>
|
||||
[OrchardFeature("Orchard.MessageBus.SqlServerServiceBroker")]
|
||||
public class SqlServerBroker : IMessageBroker, IDisposable {
|
||||
|
||||
private IWorker _worker;
|
||||
private bool _initialized;
|
||||
private object _synLock = new object();
|
||||
|
||||
private readonly Work<IRepository<MessageRecord>> _messageRecordRepository;
|
||||
private readonly Work<IClock> _clock;
|
||||
private readonly Func<IWorker> _workerFactory;
|
||||
private readonly ShellSettings _shellSettings;
|
||||
private readonly Work<IHostNameProvider> _hostNameProvider;
|
||||
|
||||
public SqlServerBroker(
|
||||
Work<IRepository<MessageRecord>> messageRecordRepository,
|
||||
Work<IClock> clock,
|
||||
Work<IHostNameProvider> hostNameProvider,
|
||||
Func<IWorker> 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<string, string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, string> 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<string, List<Action<string, string>>> _handlers = new Dictionary<string,List<Action<string,string>>>();
|
||||
|
||||
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<MessageRecord> 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<MessageRecord> 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<Action<string, string>> 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<string, string> handler) {
|
||||
GetHandlersForChannel(channel).Add(handler);
|
||||
}
|
||||
|
||||
private List<Action<string, string>> GetHandlersForChannel(string channel) {
|
||||
List<Action<string, string>> channelHandlers;
|
||||
|
||||
if(!_handlers.TryGetValue(channel, out channelHandlers)) {
|
||||
channelHandlers = new List<Action<string,string>>();
|
||||
_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<MessageRecord> GetMessages(SqlCommand command) {
|
||||
var result = new List<MessageRecord>();
|
||||
|
||||
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<MessageRecord>();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
27
src/Orchard.Web/Modules/Orchard.MessageBus/Module.txt
Normal file
27
src/Orchard.Web/Modules/Orchard.MessageBus/Module.txt
Normal file
@@ -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
|
||||
@@ -0,0 +1,154 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProductVersion>9.0.30729</ProductVersion>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectGuid>{ED715544-E649-4F48-B8EE-9368C41C3AC0}</ProjectGuid>
|
||||
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Orchard.MessageBus</RootNamespace>
|
||||
<AssemblyName>Orchard.MessageBus</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
|
||||
<MvcBuildViews>false</MvcBuildViews>
|
||||
<FileUpgradeFlags>
|
||||
</FileUpgradeFlags>
|
||||
<OldToolsVersion>4.0</OldToolsVersion>
|
||||
<UpgradeBackupLocation />
|
||||
<TargetFrameworkProfile />
|
||||
<UseIISExpress>false</UseIISExpress>
|
||||
<IISExpressSSLPort />
|
||||
<IISExpressAnonymousAuthentication />
|
||||
<IISExpressWindowsAuthentication />
|
||||
<IISExpressUseClassicPipelineMode />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="NHibernate, Version=4.0.0.4000, Culture=neutral, PublicKeyToken=aa95f207798dfdb4, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\nhibernate\NHibernate.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Web.DynamicData" />
|
||||
<Reference Include="System.Web.Mvc, Version=5.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Web" />
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
<Reference Include="System.Web.Abstractions" />
|
||||
<Reference Include="System.Web.Routing" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Web.config" />
|
||||
<Content Include="Scripts\Web.config" />
|
||||
<Content Include="Styles\Web.config" />
|
||||
<Content Include="Properties\AssemblyInfo.cs" />
|
||||
<Content Include="Module.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Orchard\Orchard.Framework.csproj">
|
||||
<Project>{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}</Project>
|
||||
<Name>Orchard.Framework</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Core\Orchard.Core.csproj">
|
||||
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
|
||||
<Name>Orchard.Core</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Controllers\" />
|
||||
<Folder Include="Views\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Brokers\SqlServer\SqlServerBroker.cs" />
|
||||
<Compile Include="Brokers\SqlServer\Worker.cs" />
|
||||
<Compile Include="Services\DistributedShellStarter.cs" />
|
||||
<Compile Include="Services\DistributedShellTrigger.cs" />
|
||||
<Compile Include="Services\HostNameProvider.cs" />
|
||||
<Compile Include="Services\IHostNameProvider.cs" />
|
||||
<Compile Include="Services\MessageBusNotificationProvider.cs" />
|
||||
<Compile Include="Handler\MessageBusHandler.cs" />
|
||||
<Compile Include="SqlServerBrokerMigrations.cs" />
|
||||
<Compile Include="Models\MessageRecord.cs" />
|
||||
<Compile Include="Services\DefaultMessageBus.cs" />
|
||||
<Compile Include="Services\DistributedSignals.cs" />
|
||||
<Compile Include="Services\IMessageBroker.cs" />
|
||||
<Compile Include="Services\IMessageBus.cs" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target> -->
|
||||
<Target Name="AfterBuild" DependsOnTargets="AfterBuildCompiler">
|
||||
<PropertyGroup>
|
||||
<AreasManifestDir>$(ProjectDir)\..\Manifests</AreasManifestDir>
|
||||
</PropertyGroup>
|
||||
<!-- If this is an area child project, uncomment the following line:
|
||||
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Child" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
|
||||
-->
|
||||
<!-- If this is an area parent project, uncomment the following lines:
|
||||
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Parent" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
|
||||
<CopyAreaManifests ManifestPath="$(AreasManifestDir)" CrossCopy="false" RenameViews="true" />
|
||||
-->
|
||||
</Target>
|
||||
<Target Name="AfterBuildCompiler" Condition="'$(MvcBuildViews)'=='true'">
|
||||
<AspNetCompiler VirtualPath="temp" PhysicalPath="$(ProjectDir)\..\$(ProjectName)" />
|
||||
</Target>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
|
||||
<WebProjectProperties>
|
||||
<UseIIS>False</UseIIS>
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>45979</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>
|
||||
</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>True</UseCustomServer>
|
||||
<CustomServerUrl>http://orchard.codeplex.com</CustomServerUrl>
|
||||
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||
</WebProjectProperties>
|
||||
</FlavorProperties>
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
@@ -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")]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<staticContent>
|
||||
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
|
||||
</staticContent>
|
||||
|
||||
<handlers accessPolicy="Script,Read">
|
||||
<!--
|
||||
iis7 - for any request to a file exists on disk, return it via native http module.
|
||||
accessPolicy 'Script' is to allow for a managed 404 page.
|
||||
-->
|
||||
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
|
||||
</handlers>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
@@ -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<IMessageBroker> messageBrokers) {
|
||||
_messageBroker = messageBrokers.FirstOrDefault();
|
||||
|
||||
Logger = NullLogger.Instance;
|
||||
}
|
||||
|
||||
public ILogger Logger { get; set; }
|
||||
|
||||
public void Subscribe(string channel, Action<string, string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ShellSettings>();
|
||||
if (shellSettings != null) {
|
||||
|
||||
// todo: this doesn't work as the new tenants list is lost right after
|
||||
var shellSettingsManagerEventHandler = scope.Resolve<IShellSettingsManagerEventHandler>();
|
||||
shellSettingsManagerEventHandler.Saved(shellSettings);
|
||||
|
||||
var orchardHost = scope.Resolve<IOrchardHost>() 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() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>(T signal) {
|
||||
base.Trigger(signal);
|
||||
_messageBus.Publish("Signal", signal.ToString());
|
||||
}
|
||||
|
||||
IVolatileToken ISignals.When<T>(T signal) {
|
||||
return base.When(signal);
|
||||
}
|
||||
|
||||
public void Activated() {
|
||||
_messageBus.Subscribe("Signal", (channel, message) => {
|
||||
base.Trigger(message);
|
||||
});
|
||||
}
|
||||
|
||||
public void Terminating() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<string, string> handler);
|
||||
void Publish(string channel, string message);
|
||||
}
|
||||
}
|
||||
@@ -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<string, string> handler);
|
||||
void Publish(string channel, string message);
|
||||
}
|
||||
}
|
||||
@@ -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<IMessageBroker> _messageBrokers;
|
||||
|
||||
public MessageBusNotificationProvider(IEnumerable<IMessageBroker> messageBrokers) {
|
||||
_messageBrokers = messageBrokers;
|
||||
T = NullLocalizer.Instance;
|
||||
}
|
||||
|
||||
public Localizer T { get; set; }
|
||||
|
||||
public IEnumerable<NotifyEntry> 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<int>("Id", c => c.PrimaryKey().Identity())
|
||||
.Column<string>("Publisher", c => c.WithLength(255))
|
||||
.Column<string>("Channel", c => c.WithLength(255))
|
||||
.Column<string>("Message", c => c.Unlimited())
|
||||
.Column<DateTime>("CreatedUtc")
|
||||
);
|
||||
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Orchard.Web/Modules/Orchard.MessageBus/Styles/Web.config
Normal file
16
src/Orchard.Web/Modules/Orchard.MessageBus/Styles/Web.config
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<system.webServer>
|
||||
<staticContent>
|
||||
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
|
||||
</staticContent>
|
||||
|
||||
<handlers accessPolicy="Script,Read">
|
||||
<!--
|
||||
iis7 - for any request to a file exists on disk, return it via native http module.
|
||||
accessPolicy 'Script' is to allow for a managed 404 page.
|
||||
-->
|
||||
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
|
||||
</handlers>
|
||||
</system.webServer>
|
||||
</configuration>
|
||||
41
src/Orchard.Web/Modules/Orchard.MessageBus/Web.config
Normal file
41
src/Orchard.Web/Modules/Orchard.MessageBus/Web.config
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0"?>
|
||||
<configuration>
|
||||
|
||||
<configSections>
|
||||
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
|
||||
<remove name="host" />
|
||||
<remove name="pages" />
|
||||
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
|
||||
<system.web.webPages.razor>
|
||||
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
|
||||
<pages pageBaseType="Orchard.Mvc.ViewEngines.Razor.WebViewPage">
|
||||
<namespaces>
|
||||
<add namespace="System.Web.Mvc" />
|
||||
<add namespace="System.Web.Mvc.Ajax" />
|
||||
<add namespace="System.Web.Mvc.Html" />
|
||||
<add namespace="System.Web.Routing" />
|
||||
<add namespace="System.Web.WebPages" />
|
||||
<add namespace="System.Linq"/>
|
||||
<add namespace="System.Collections.Generic"/>
|
||||
<add namespace="Orchard.Mvc.Html"/>
|
||||
</namespaces>
|
||||
</pages>
|
||||
</system.web.webPages.razor>
|
||||
|
||||
<system.web>
|
||||
<compilation targetFramework="4.5">
|
||||
<assemblies>
|
||||
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||
<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
|
||||
<add assembly="System.Web.Mvc, Version=5.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
|
||||
<add assembly="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||
</assemblies>
|
||||
</compilation>
|
||||
</system.web>
|
||||
|
||||
</configuration>
|
||||
@@ -12,4 +12,4 @@ Features:
|
||||
Orchard.OutputCache.Database:
|
||||
Description: Stores output cache data in the database.
|
||||
Category: Performance
|
||||
Dependencies: Orchard.OutputCache
|
||||
Dependencies: Orchard.OutputCache
|
||||
|
||||
@@ -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<T>(string key) {
|
||||
var json = Database.StringGet(GetLocalizedKey(key));
|
||||
return JsonConvert.DeserializeObject<T>(json);
|
||||
}
|
||||
|
||||
public void Put<T>(string key, T value) {
|
||||
var json = JsonConvert.SerializeObject(value);
|
||||
Database.StringSet(GetLocalizedKey(key), json, null);
|
||||
}
|
||||
|
||||
public void Put<T>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace Orchard.Redis.Configuration {
|
||||
|
||||
public interface IRedisConnectionProvider : ISingletonDependency {
|
||||
ConnectionMultiplexer GetConnection(string connectionString);
|
||||
string GetConnectionString(string service);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<string, ConnectionMultiplexer> _connectionMultiplexers = new ConcurrentDictionary<string, ConnectionMultiplexer>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, ConcurrentBag<Action<string, string>>> _handlers = new ConcurrentDictionary<string, ConcurrentBag<Action<string, string>>>();
|
||||
|
||||
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<string, string> handler) {
|
||||
|
||||
try {
|
||||
var channelHandlers = _handlers.GetOrAdd(channel, c => {
|
||||
return new ConcurrentBag<Action<string, string>>();
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
27
src/Orchard.Web/Modules/Orchard.Redis/Module.txt
Normal file
27
src/Orchard.Web/Modules/Orchard.Redis/Module.txt
Normal file
@@ -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
|
||||
159
src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj
Normal file
159
src/Orchard.Web/Modules/Orchard.Redis/Orchard.Redis.csproj
Normal file
@@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProductVersion>9.0.30729</ProductVersion>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectGuid>{2C5EB8B3-A313-413D-BAA0-5C21D2A6EC6E}</ProjectGuid>
|
||||
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
|
||||
<OutputType>Library</OutputType>
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>Orchard.Redis</RootNamespace>
|
||||
<AssemblyName>Orchard.Redis</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.1</TargetFrameworkVersion>
|
||||
<MvcBuildViews>false</MvcBuildViews>
|
||||
<FileUpgradeFlags>
|
||||
</FileUpgradeFlags>
|
||||
<OldToolsVersion>4.0</OldToolsVersion>
|
||||
<UpgradeBackupLocation />
|
||||
<TargetFrameworkProfile />
|
||||
<UseIISExpress>false</UseIISExpress>
|
||||
<IISExpressSSLPort />
|
||||
<IISExpressAnonymousAuthentication />
|
||||
<IISExpressWindowsAuthentication />
|
||||
<IISExpressUseClassicPipelineMode />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>..\..\..\..\lib\newtonsoft.json\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="NHibernate, Version=4.0.0.4000, Culture=neutral, PublicKeyToken=aa95f207798dfdb4, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\nhibernate\NHibernate.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="StackExchange.Redis">
|
||||
<HintPath>..\..\..\..\lib\redis\StackExchange.Redis.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.ComponentModel.DataAnnotations">
|
||||
<RequiredTargetFramework>3.5</RequiredTargetFramework>
|
||||
</Reference>
|
||||
<Reference Include="System.Web.DynamicData" />
|
||||
<Reference Include="System.Web.Mvc, Version=5.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<SpecificVersion>False</SpecificVersion>
|
||||
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Web" />
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
<Reference Include="System.Web.Abstractions" />
|
||||
<Reference Include="System.Web.Routing" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="Web.config" />
|
||||
<Content Include="Properties\AssemblyInfo.cs" />
|
||||
<Content Include="Module.txt" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\Orchard\Orchard.Framework.csproj">
|
||||
<Project>{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}</Project>
|
||||
<Name>Orchard.Framework</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\..\Core\Orchard.Core.csproj">
|
||||
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
|
||||
<Name>Orchard.Core</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Orchard.Caching\Orchard.Caching.csproj">
|
||||
<Project>{7528BF74-25C7-4ABE-883A-443B4EEC4776}</Project>
|
||||
<Name>Orchard.Caching</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Orchard.MessageBus\Orchard.MessageBus.csproj">
|
||||
<Project>{ed715544-e649-4f48-b8ee-9368c41c3ac0}</Project>
|
||||
<Name>Orchard.MessageBus</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Orchard.OutputCache\Orchard.OutputCache.csproj">
|
||||
<Project>{6e444ff1-a47c-4cf6-bb3f-507c8ebd776d}</Project>
|
||||
<Name>Orchard.OutputCache</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Caching\RedisCacheStorageProvider.cs" />
|
||||
<Compile Include="Extensions\RedisDatabaseExtensions.cs" />
|
||||
<Compile Include="Configuration\IRedisConnectionProvider.cs" />
|
||||
<Compile Include="Configuration\RedisConnectionProvider.cs" />
|
||||
<Compile Include="MessageBus\RedisMessageBusBroker.cs" />
|
||||
<Compile Include="OutputCache\RedisOutputCacheStorageProvider.cs" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
|
||||
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target> -->
|
||||
<Target Name="AfterBuild" DependsOnTargets="AfterBuildCompiler">
|
||||
<PropertyGroup>
|
||||
<AreasManifestDir>$(ProjectDir)\..\Manifests</AreasManifestDir>
|
||||
</PropertyGroup>
|
||||
<!-- If this is an area child project, uncomment the following line:
|
||||
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Child" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
|
||||
-->
|
||||
<!-- If this is an area parent project, uncomment the following lines:
|
||||
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Parent" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
|
||||
<CopyAreaManifests ManifestPath="$(AreasManifestDir)" CrossCopy="false" RenameViews="true" />
|
||||
-->
|
||||
</Target>
|
||||
<Target Name="AfterBuildCompiler" Condition="'$(MvcBuildViews)'=='true'">
|
||||
<AspNetCompiler VirtualPath="temp" PhysicalPath="$(ProjectDir)\..\$(ProjectName)" />
|
||||
</Target>
|
||||
<ProjectExtensions>
|
||||
<VisualStudio>
|
||||
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
|
||||
<WebProjectProperties>
|
||||
<UseIIS>False</UseIIS>
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>45979</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>
|
||||
</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>True</UseCustomServer>
|
||||
<CustomServerUrl>http://orchard.codeplex.com</CustomServerUrl>
|
||||
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
|
||||
</WebProjectProperties>
|
||||
</FlavorProperties>
|
||||
</VisualStudio>
|
||||
</ProjectExtensions>
|
||||
</Project>
|
||||
@@ -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<string> _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<CacheItem>(value);
|
||||
}
|
||||
|
||||
public IEnumerable<CacheItem> 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("*"));
|
||||
}
|
||||
|
||||
/// <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 + ":OutputCache:" + 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("");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")]
|
||||
|
||||
41
src/Orchard.Web/Modules/Orchard.Redis/Web.config
Normal file
41
src/Orchard.Web/Modules/Orchard.Redis/Web.config
Normal file
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0"?>
|
||||
<configuration>
|
||||
|
||||
<configSections>
|
||||
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
|
||||
<remove name="host" />
|
||||
<remove name="pages" />
|
||||
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
|
||||
<system.web.webPages.razor>
|
||||
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
|
||||
<pages pageBaseType="Orchard.Mvc.ViewEngines.Razor.WebViewPage">
|
||||
<namespaces>
|
||||
<add namespace="System.Web.Mvc" />
|
||||
<add namespace="System.Web.Mvc.Ajax" />
|
||||
<add namespace="System.Web.Mvc.Html" />
|
||||
<add namespace="System.Web.Routing" />
|
||||
<add namespace="System.Web.WebPages" />
|
||||
<add namespace="System.Linq"/>
|
||||
<add namespace="System.Collections.Generic"/>
|
||||
<add namespace="Orchard.Mvc.Html"/>
|
||||
</namespaces>
|
||||
</pages>
|
||||
</system.web.webPages.razor>
|
||||
|
||||
<system.web>
|
||||
<compilation targetFramework="4.5">
|
||||
<assemblies>
|
||||
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||
<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
|
||||
<add assembly="System.Web.Mvc, Version=5.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
|
||||
<add assembly="System.Web.WebPages, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
|
||||
</assemblies>
|
||||
</compilation>
|
||||
</system.web>
|
||||
|
||||
</configuration>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user