diff --git a/AsyncGenerator.yml b/AsyncGenerator.yml index 9b858a37..9489bf4e 100644 --- a/AsyncGenerator.yml +++ b/AsyncGenerator.yml @@ -1,4 +1,33 @@ projects: +- filePath: CoreDistributedCache\NHibernate.Caches.CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.csproj + targetFramework: net461 + concurrentRun: true + applyChanges: true + analyzation: + methodConversion: + - conversion: Ignore + hasAttributeName: ObsoleteAttribute + callForwarding: true + cancellationTokens: + guards: true + methodParameter: + - anyInterfaceRule: PubliclyExposedType + parameter: Optional + - parameter: Optional + rule: PubliclyExposedType + - parameter: Required + scanMethodBody: true + scanForMissingAsyncMembers: + - all: true + transformation: + configureAwaitArgument: false + localFunctions: true + asyncLock: + type: NHibernate.Util.AsyncLock + methodName: LockAsync + registerPlugin: + - type: AsyncGenerator.Core.Plugins.EmptyRegionRemover + assemblyName: AsyncGenerator.Core - filePath: CoreMemoryCache\NHibernate.Caches.CoreMemoryCache\NHibernate.Caches.CoreMemoryCache.csproj targetFramework: net461 concurrentRun: true @@ -293,6 +322,47 @@ registerPlugin: - type: AsyncGenerator.Core.Plugins.NUnitAsyncCounterpartsFinder assemblyName: AsyncGenerator.Core +- filePath: CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Tests\NHibernate.Caches.CoreDistributedCache.Tests.csproj + targetFramework: net461 + concurrentRun: true + applyChanges: true + analyzation: + methodConversion: + - conversion: Ignore + hasAttributeName: OneTimeSetUpAttribute + - conversion: Ignore + hasAttributeName: OneTimeTearDownAttribute + - conversion: Ignore + hasAttributeName: SetUpAttribute + - conversion: Ignore + hasAttributeName: TearDownAttribute + - conversion: Smart + hasAttributeName: TestAttribute + - conversion: Smart + hasAttributeName: TheoryAttribute + preserveReturnType: + - hasAttributeName: TestAttribute + - hasAttributeName: TheoryAttribute + typeConversion: + - conversion: Ignore + hasAttributeName: IgnoreAttribute + - conversion: Partial + hasAttributeName: TestFixtureAttribute + - conversion: Partial + anyBaseTypeRule: HasTestFixtureAttribute + ignoreSearchForMethodReferences: + - hasAttributeName: TheoryAttribute + - hasAttributeName: TestAttribute + cancellationTokens: + withoutCancellationToken: + - hasAttributeName: TestAttribute + - hasAttributeName: TheoryAttribute + scanMethodBody: true + scanForMissingAsyncMembers: + - all: true + registerPlugin: + - type: AsyncGenerator.Core.Plugins.NUnitAsyncCounterpartsFinder + assemblyName: AsyncGenerator.Core - filePath: CoreMemoryCache\NHibernate.Caches.CoreMemoryCache.Tests\NHibernate.Caches.CoreMemoryCache.Tests.csproj targetFramework: net461 concurrentRun: true diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memcached/AssemblyInfo.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memcached/AssemblyInfo.cs new file mode 100644 index 00000000..bd9d30e7 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memcached/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System; +using System.Reflection; + +[assembly: CLSCompliant(true)] +[assembly: AssemblyDelaySign(false)] \ No newline at end of file diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memcached/MemcachedFactory.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memcached/MemcachedFactory.cs new file mode 100644 index 00000000..0d18e98f --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memcached/MemcachedFactory.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using Enyim.Caching; +using Enyim.Caching.Configuration; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +namespace NHibernate.Caches.CoreDistributedCache.Memcached +{ + /// + /// A Memcached distributed cache factory. + /// + public class MemcachedFactory : IDistributedCacheFactory + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(MemcachedFactory)); + private const string _configuration = "configuration"; + + private readonly IDistributedCache _cache; + + /// + /// Constructor with configuration properties. It supports configuration, which has to be a JSON string + /// structured like the value part of the "enyimMemcached" property in an appsettings.json file. + /// + /// The configurations properties. + public MemcachedFactory(IDictionary properties) : this() + { + MemcachedClientOptions options; + if (properties != null && properties.TryGetValue(_configuration, out var configuration) && !string.IsNullOrWhiteSpace(configuration)) + { + options = JsonConvert.DeserializeObject(configuration); + } + else + { + Log.Warn("No {0} property provided", _configuration); + options = new MemcachedClientOptions(); + } + + var loggerFactory = new LoggerFactory(); + + _cache = new MemcachedClient(loggerFactory, new MemcachedClientConfiguration(loggerFactory, options)); + } + + private MemcachedFactory() + { + Constraints = new CacheConstraints + { + MaxKeySize = 250, + KeySanitizer = SanitizeKey + }; + } + + // According to https://groups.google.com/forum/#!topic/memcached/Tz1RE0FUbNA, + // memcached key can't contain space, newline, return, tab, vertical tab or form feed. + // Since keys contains entity identifiers which may be anything, purging them all. + private static readonly char[] ForbiddenChar = new [] { ' ', '\n', '\r', '\t', '\v', '\f' }; + + private static string SanitizeKey(string key) + { + foreach (var forbidden in ForbiddenChar) + { + key = key.Replace(forbidden, '-'); + } + return key; + } + + /// + public CacheConstraints Constraints { get; } + + /// + [CLSCompliant(false)] + public IDistributedCache BuildCache() + { + return _cache; + } + + private class LoggerFactory : Microsoft.Extensions.Logging.ILoggerFactory + { + public void Dispose() + { + } + + public ILogger CreateLogger(string categoryName) + { + return new LoggerWrapper(NHibernateLogger.For(categoryName)); + } + + public void AddProvider(ILoggerProvider provider) + { + } + } + + private class LoggerWrapper : ILogger + { + private readonly INHibernateLogger _logger; + + public LoggerWrapper(INHibernateLogger logger) + { + _logger = logger; + } + + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + if (formatter == null) + throw new ArgumentNullException(nameof(formatter)); + + _logger.Log( + TranslateLevel(logLevel), + new NHibernateLogValues("EventId {0}: {1}", new object[] { eventId, formatter(state, exception) }), + // Avoid double logging of exception by not providing it to the logger, but only to the formatter. + null); + } + + public bool IsEnabled(LogLevel logLevel) + => _logger.IsEnabled(TranslateLevel(logLevel)); + + public IDisposable BeginScope(TState state) + => NoopScope.Instance; + + private NHibernateLogLevel TranslateLevel(LogLevel level) + { + switch (level) + { + case LogLevel.None: + return NHibernateLogLevel.None; + case LogLevel.Trace: + return NHibernateLogLevel.Trace; + case LogLevel.Debug: + return NHibernateLogLevel.Debug; + case LogLevel.Information: + return NHibernateLogLevel.Info; + case LogLevel.Warning: + return NHibernateLogLevel.Warn; + case LogLevel.Error: + return NHibernateLogLevel.Error; + case LogLevel.Critical: + return NHibernateLogLevel.Fatal; + } + + return NHibernateLogLevel.Trace; + } + + private class NoopScope : IDisposable + { + public static readonly NoopScope Instance = new NoopScope(); + + public void Dispose() + { + } + } + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memcached/NHibernate.Caches.CoreDistributedCache.Memcached.csproj b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memcached/NHibernate.Caches.CoreDistributedCache.Memcached.csproj new file mode 100644 index 00000000..aafefe85 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memcached/NHibernate.Caches.CoreDistributedCache.Memcached.csproj @@ -0,0 +1,34 @@ + + + + NHibernate.Caches.CoreDistributedCache.Memcached + NHibernate.Caches.CoreDistributedCache.Memcached + Memcached cache provider for NHibernate using .Net Core IDistributedCache (EnyimMemcachedCore). + + net461;netstandard2.0 + False + true + * New feature + * #28 - Add a .Net Core DistributedCache + + + NETFX;$(DefineConstants) + + + + + + + + + + ./NHibernate.Caches.readme.md + + + ./NHibernate.Caches.license.txt + + + + + + diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/AssemblyInfo.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/AssemblyInfo.cs new file mode 100644 index 00000000..bd9d30e7 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System; +using System.Reflection; + +[assembly: CLSCompliant(true)] +[assembly: AssemblyDelaySign(false)] \ No newline at end of file diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/MemoryFactory.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/MemoryFactory.cs new file mode 100644 index 00000000..65937ce2 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/MemoryFactory.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace NHibernate.Caches.CoreDistributedCache.Memory +{ + /// + /// A memory "distributed" cache factory. Use for testing purpose. Otherwise consider using CoreMemoryCache + /// instead. + /// + public class MemoryFactory : IDistributedCacheFactory + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(MemoryFactory)); + private const string _expirationScanFrequency = "expiration-scan-frequency"; + private const string _sizeLimit = "size-limit"; + + private readonly IDistributedCache _cache; + + /// + /// Default constructor. + /// + public MemoryFactory() : this(null) + { + } + + /// + /// Constructor with explicit configuration properties. + /// + /// See . + /// See . + public MemoryFactory(TimeSpan? expirationScanFrequency, long? sizeLimit) + { + var options = new Options(); + if (expirationScanFrequency.HasValue) + options.ExpirationScanFrequency = expirationScanFrequency.Value; + if (sizeLimit.HasValue) + options.SizeLimit = sizeLimit.Value; + + _cache = new MemoryDistributedCache(options); + } + + /// + /// Constructor with configuration properties. It supports expiration-scan-frequency and + /// size-limit properties. See and + /// . + /// + /// The configurations properties. + /// + /// + /// If expiration-scan-frequency is provided as an integer, this integer will be used as a number + /// of minutes. Otherwise the setting will be parsed as a . + /// + /// size-limit has to be an integer, expressing the size limit in bytes. + /// + public MemoryFactory(IDictionary properties) + { + var options = new Options(); + + if (properties != null) + { + if (properties.TryGetValue(_expirationScanFrequency, out var esf)) + { + if (esf != null) + { + if (int.TryParse(esf, out var minutes)) + options.ExpirationScanFrequency = TimeSpan.FromMinutes(minutes); + else if (TimeSpan.TryParse(esf, out var expirationScanFrequency)) + options.ExpirationScanFrequency = expirationScanFrequency; + else + Log.Warn( + "Invalid value '{0}' for {1} setting: it is neither an int nor a TimeSpan. Ignoring.", + esf, _expirationScanFrequency); + } + else + { + Log.Warn("Invalid property {0}: it lacks a value. Ignoring.", _expirationScanFrequency); + } + } + + if (properties.TryGetValue(_sizeLimit, out var sl)) + { + if (sl != null) + { + if (long.TryParse(sl, out var bytes)) + options.SizeLimit = bytes; + else + Log.Warn( + "Invalid value '{0}' for {1} setting: it is not an integer. Ignoring.", + sl, _sizeLimit); + } + else + { + Log.Warn("Invalid property {0}: it lacks a value. Ignoring.", _sizeLimit); + } + } + } + + _cache = new MemoryDistributedCache(options); + } + + /// + [CLSCompliant(false)] + public IDistributedCache BuildCache() + { + // Always yields the same instance: its underlying implementation is a MemoryCache which regularly spawn + // a background task for expiring items. This avoids creating many instances, thus avoiding potentially + // spawning many such background tasks at once. + // This also allows to share the cache between all session factories of a process, thus emulating a + // distributed aspect. + return _cache; + } + + /// + public CacheConstraints Constraints => null; + + private class Options : MemoryDistributedCacheOptions, IOptions + { + MemoryDistributedCacheOptions IOptions.Value => this; + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/NHibernate.Caches.CoreDistributedCache.Memory.csproj b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/NHibernate.Caches.CoreDistributedCache.Memory.csproj new file mode 100644 index 00000000..c9698ecc --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/NHibernate.Caches.CoreDistributedCache.Memory.csproj @@ -0,0 +1,36 @@ + + + + NHibernate.Caches.CoreDistributedCache.Memory + NHibernate.Caches.CoreDistributedCache.Memory + Memory cache provider for NHibernate using .Net Core IDistributedCache (Microsoft.Extensions.Caching.Redis). +Meant for testing purpose, consider NHibernate.Caches.CoreMemoryCache for other usages. + + net461;netstandard2.0 + True + ..\..\NHibernate.Caches.snk + true + * New feature + * #28 - Add a .Net Core DistributedCache + + + NETFX;$(DefineConstants) + + + + + + + + + + ./NHibernate.Caches.readme.md + + + ./NHibernate.Caches.license.txt + + + + + + diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/AssemblyInfo.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/AssemblyInfo.cs new file mode 100644 index 00000000..bd9d30e7 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System; +using System.Reflection; + +[assembly: CLSCompliant(true)] +[assembly: AssemblyDelaySign(false)] \ No newline at end of file diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/NHibernate.Caches.CoreDistributedCache.Redis.csproj b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/NHibernate.Caches.CoreDistributedCache.Redis.csproj new file mode 100644 index 00000000..3d945aac --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/NHibernate.Caches.CoreDistributedCache.Redis.csproj @@ -0,0 +1,35 @@ + + + + NHibernate.Caches.CoreDistributedCache.Redis + NHibernate.Caches.CoreDistributedCache.Redis + Redis cache provider for NHibernate using .Net Core IDistributedCache (Microsoft.Extensions.Caching.Redis). + + net461;netstandard2.0 + True + ..\..\NHibernate.Caches.snk + true + * New feature + * #28 - Add a .Net Core DistributedCache + + + NETFX;$(DefineConstants) + + + + + + + + + + ./NHibernate.Caches.readme.md + + + ./NHibernate.Caches.license.txt + + + + + + diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/RedisFactory.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/RedisFactory.cs new file mode 100644 index 00000000..38418c4c --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/RedisFactory.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Redis; + +namespace NHibernate.Caches.CoreDistributedCache.Redis +{ + /// + /// A Redis distributed cache factory. See . + /// + public class RedisFactory : IDistributedCacheFactory + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(RedisFactory)); + private const string _configuration = "configuration"; + private const string _instanceName = "instance-name"; + + private readonly RedisCacheOptions _options; + + /// + /// Constructor with explicit configuration properties. + /// + /// See . + /// See . + public RedisFactory(string configuration, string instanceName) + { + _options = new RedisCacheOptions + { + Configuration = configuration, + InstanceName = instanceName + }; + } + + /// + /// Constructor with configuration properties. It supports configuration and + /// instance-name properties. See and + /// . + /// + /// The configurations properties. + public RedisFactory(IDictionary properties) + { + _options = new RedisCacheOptions(); + + if (properties == null) + return; + + if (properties.TryGetValue(_configuration, out var configuration)) + { + _options.Configuration = configuration; + Log.Info("Configuration set as '{0}'", configuration); + } + else + { + // Configuration is supposed to be mandatory. + Log.Warn("No {0} property provided", _configuration); + } + + if (properties.TryGetValue(_instanceName, out var instanceName)) + { + _options.InstanceName = instanceName; + Log.Info("InstanceName set as '{0}'", instanceName); + } + else + Log.Info("No {0} property provided", _instanceName); + } + + /// + public CacheConstraints Constraints => null; + + /// + [CLSCompliant(false)] + public IDistributedCache BuildCache() + { + // According to https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed#the-idistributedcache-interface + // (see its paragraph end note) there is no need for a singleton lifetime. + return new RedisCache(_options); + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/AssemblyInfo.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/AssemblyInfo.cs new file mode 100644 index 00000000..bd9d30e7 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System; +using System.Reflection; + +[assembly: CLSCompliant(true)] +[assembly: AssemblyDelaySign(false)] \ No newline at end of file diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/NHibernate.Caches.CoreDistributedCache.SqlServer.csproj b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/NHibernate.Caches.CoreDistributedCache.SqlServer.csproj new file mode 100644 index 00000000..4af24545 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/NHibernate.Caches.CoreDistributedCache.SqlServer.csproj @@ -0,0 +1,35 @@ + + + + NHibernate.Caches.CoreDistributedCache.SqlServer + NHibernate.Caches.CoreDistributedCache.SqlServer + SQL Server cache provider for NHibernate using .Net Core IDistributedCache (Microsoft.Extensions.Caching.SqlServer). + + net461;netstandard2.0 + True + ..\..\NHibernate.Caches.snk + true + * New feature + * #28 - Add a .Net Core DistributedCache + + + NETFX;$(DefineConstants) + + + + + + + + + + ./NHibernate.Caches.readme.md + + + ./NHibernate.Caches.license.txt + + + + + + diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/SqlServerFactory.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/SqlServerFactory.cs new file mode 100644 index 00000000..1d688247 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/SqlServerFactory.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.SqlServer; + +namespace NHibernate.Caches.CoreDistributedCache.SqlServer +{ + /// + /// A Redis distributed cache factory. See . + /// + public class SqlServerFactory : IDistributedCacheFactory + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(SqlServerFactory)); + private const string _connectionString = "connection-string"; + private const string _schemaName = "schema-name"; + private const string _tableName = "table-name"; + private const string _expiredItemsDeletionInterval = "expired-items-deletion-interval"; + + private readonly SqlServerCacheOptions _options; + + /// + /// Constructor with explicit configuration properties. + /// + /// See . + /// See . + /// See . + /// See . + public SqlServerFactory( + string connectionString, string schemaName, string tableName, TimeSpan? expiredItemsDeletionInterval) + { + _options = new SqlServerCacheOptions + { + ConnectionString = connectionString, + SchemaName = schemaName, + TableName = tableName, + ExpiredItemsDeletionInterval = expiredItemsDeletionInterval + }; + } + + /// + /// Constructor with configuration properties. It supports connection-string, schema-name, + /// table-name and expired-items-deletion-interval properties. + /// See . + /// + /// The configurations properties. + /// + /// If expired-items-deletion-interval is provided as an integer, this integer will be used as a number + /// of minutes. Otherwise the setting will be parsed as a . + /// + public SqlServerFactory(IDictionary properties) + { + _options = new SqlServerCacheOptions(); + + if (properties == null) + return; + + if (properties.TryGetValue(_connectionString, out var connectionString)) + { + _options.ConnectionString = connectionString; + Log.Info("ConnectionString set as '{0}'", connectionString); + } + else + Log.Warn("No {0} property provided", _connectionString); + + if (properties.TryGetValue(_schemaName, out var schemaName)) + { + _options.SchemaName = schemaName; + Log.Info("SchemaName set as '{0}'", schemaName); + } + else + Log.Warn("No {0} property provided", _schemaName); + + if (properties.TryGetValue(_tableName, out var tableName)) + { + _options.TableName = tableName; + Log.Info("TableName set as '{0}'", tableName); + } + else + Log.Warn("No {0} property provided", _tableName); + + if (properties.TryGetValue(_expiredItemsDeletionInterval, out var eidi)) + { + if (eidi != null) + { + if (int.TryParse(eidi, out var minutes)) + _options.ExpiredItemsDeletionInterval = TimeSpan.FromMinutes(minutes); + else if (TimeSpan.TryParse(eidi, out var expirationScanFrequency)) + _options.ExpiredItemsDeletionInterval = expirationScanFrequency; + else + Log.Warn( + "Invalid value '{0}' for {1} setting: it is neither an int nor a TimeSpan. Ignoring.", + eidi, _expiredItemsDeletionInterval); + } + else + { + Log.Warn("Invalid property {0}: it lacks a value. Ignoring.", _expiredItemsDeletionInterval); + } + } + } + + /// + public CacheConstraints Constraints { get; } = new CacheConstraints { MaxKeySize = 449 }; + + /// + [CLSCompliant(false)] + public IDistributedCache BuildCache() + { + // According to https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed#the-idistributedcache-interface + // (see its paragraph end note) there is no need for a singleton lifetime. + return new SqlServerCache(_options); + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/App.config b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/App.config new file mode 100644 index 00000000..d51086a8 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/App.config @@ -0,0 +1,71 @@ + + + +
+
+ + + + + + 00:10:00 + 1048576 + + + + + + + + + + + + + + NHibernate.Connection.DriverConnectionProvider + NHibernate.Dialect.MsSql2005Dialect + NHibernate.Driver.SqlClientDriver + + Server=localhost;initial catalog=nhibernate;Integrated Security=SSPI + + NHibernate.Caches.CoreDistributedCache.CoreDistributedCacheProvider,NHibernate.Caches.CoreDistributedCache + + + diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/Async/CoreDistributedCacheFixture.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/Async/CoreDistributedCacheFixture.cs new file mode 100644 index 00000000..0da5ea55 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/Async/CoreDistributedCacheFixture.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +#region License + +// +// CoreDistributedCache - A cache provider for NHibernate using Microsoft.Extensions.Caching.Distributed.IDistributedCache. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// + +#endregion + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Caching.Distributed; +using NHibernate.Cache; +using NHibernate.Caches.Common.Tests; +using NUnit.Framework; +using NSubstitute; + +namespace NHibernate.Caches.CoreDistributedCache.Tests +{ + using System.Threading.Tasks; + using System.Threading; + public partial class CoreDistributedCacheFixture : CacheFixture + { + + [Test] + public async Task MaxKeySizeAsync() + { + var distribCache = Substitute.For(); + const int maxLength = 20; + var cache = new CoreDistributedCache(distribCache, new CacheConstraints { MaxKeySize = maxLength }, "foo", + new Dictionary()); + await (cache.PutAsync(new string('k', maxLength * 2), "test", CancellationToken.None)); + await (distribCache.Received().SetAsync(Arg.Is(k => k.Length <= maxLength), Arg.Any(), + Arg.Any())); + } + + [Test] + public async Task KeySanitizerAsync() + { + var distribCache = Substitute.For(); + Func keySanitizer = s => s.Replace('a', 'b'); + var cache = new CoreDistributedCache(distribCache, new CacheConstraints { KeySanitizer = keySanitizer }, "foo", + new Dictionary()); + await (cache.PutAsync("-abc-", "test", CancellationToken.None)); + await (distribCache.Received().SetAsync(Arg.Is(k => k.Contains(keySanitizer("-abc-"))), Arg.Any(), + Arg.Any())); + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheFixture.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheFixture.cs new file mode 100644 index 00000000..0ba6ec7e --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheFixture.cs @@ -0,0 +1,66 @@ +#region License + +// +// CoreDistributedCache - A cache provider for NHibernate using Microsoft.Extensions.Caching.Distributed.IDistributedCache. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// + +#endregion + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Caching.Distributed; +using NHibernate.Cache; +using NHibernate.Caches.Common.Tests; +using NUnit.Framework; +using NSubstitute; + +namespace NHibernate.Caches.CoreDistributedCache.Tests +{ + [TestFixture] + public partial class CoreDistributedCacheFixture : CacheFixture + { + protected override bool SupportsSlidingExpiration => true; + protected override bool SupportsClear => false; + + protected override Func ProviderBuilder => + () => new CoreDistributedCacheProvider(); + + [Test] + public void MaxKeySize() + { + var distribCache = Substitute.For(); + const int maxLength = 20; + var cache = new CoreDistributedCache(distribCache, new CacheConstraints { MaxKeySize = maxLength }, "foo", + new Dictionary()); + cache.Put(new string('k', maxLength * 2), "test"); + distribCache.Received().Set(Arg.Is(k => k.Length <= maxLength), Arg.Any(), + Arg.Any()); + } + + [Test] + public void KeySanitizer() + { + var distribCache = Substitute.For(); + Func keySanitizer = s => s.Replace('a', 'b'); + var cache = new CoreDistributedCache(distribCache, new CacheConstraints { KeySanitizer = keySanitizer }, "foo", + new Dictionary()); + cache.Put("-abc-", "test"); + distribCache.Received().Set(Arg.Is(k => k.Contains(keySanitizer("-abc-"))), Arg.Any(), + Arg.Any()); + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheProviderFixture.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheProviderFixture.cs new file mode 100644 index 00000000..391336e7 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheProviderFixture.cs @@ -0,0 +1,68 @@ +#region License + +// +// CoreDistributedCache - A cache provider for NHibernate using Microsoft.Extensions.Caching.Distributed.IDistributedCache. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// + +#endregion + +using System; +using System.Collections.Generic; +using NHibernate.Cache; +using NHibernate.Caches.Common.Tests; +using NUnit.Framework; + +namespace NHibernate.Caches.CoreDistributedCache.Tests +{ + [TestFixture] + public class CoreDistributedCacheProviderFixture : CacheProviderFixture + { + protected override Func ProviderBuilder => + () => new CoreDistributedCacheProvider(); + + [Test] + public void TestBuildCacheFromConfig() + { + var cache = DefaultProvider.BuildCache("foo", null); + Assert.That(cache, Is.Not.Null, "pre-configured cache not found"); + } + + [Test] + public void TestExpiration() + { + var cache = DefaultProvider.BuildCache("foo", null) as CoreDistributedCache; + Assert.That(cache, Is.Not.Null, "pre-configured foo cache not found"); + Assert.That(cache.Expiration, Is.EqualTo(TimeSpan.FromSeconds(500)), "Unexpected expiration value for foo region"); + + cache = (CoreDistributedCache) DefaultProvider.BuildCache("noExplicitExpiration", null); + Assert.That(cache.Expiration, Is.EqualTo(TimeSpan.FromSeconds(300)), + "Unexpected expiration value for noExplicitExpiration region"); + Assert.That(cache.UseSlidingExpiration, Is.True, "Unexpected sliding value for noExplicitExpiration region"); + + cache = (CoreDistributedCache) DefaultProvider + .BuildCache("noExplicitExpiration", new Dictionary { { "expiration", "100" } }); + Assert.That(cache.Expiration, Is.EqualTo(TimeSpan.FromSeconds(100)), + "Unexpected expiration value for noExplicitExpiration region with default expiration"); + + cache = (CoreDistributedCache) DefaultProvider + .BuildCache("noExplicitExpiration", + new Dictionary { { Cfg.Environment.CacheDefaultExpiration, "50" } }); + Assert.That(cache.Expiration, Is.EqualTo(TimeSpan.FromSeconds(50)), + "Unexpected expiration value for noExplicitExpiration region with cache.default_expiration"); + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheSectionHandlerFixture.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheSectionHandlerFixture.cs new file mode 100644 index 00000000..4694ead1 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheSectionHandlerFixture.cs @@ -0,0 +1,81 @@ +#region License + +// +// CoreDistributedCache - A cache provider for NHibernate using Microsoft.Extensions.Caching.Distributed.IDistributedCache. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// + +#endregion + +using System.Xml; +using NUnit.Framework; + +namespace NHibernate.Caches.CoreDistributedCache.Tests +{ + [TestFixture] + public class CoreDistributedCacheSectionHandlerFixture + { + private static XmlNode GetConfigurationSection(string xml) + { + var doc = new XmlDocument(); + doc.LoadXml(xml); + return doc.DocumentElement; + } + + [Test] + public void TestGetConfigNullSection() + { + var handler = new CoreDistributedCacheSectionHandler(); + var section = new XmlDocument(); + var result = handler.Create(null, null, section); + Assert.That(result, Is.Not.Null, "result"); + Assert.That(result, Is.InstanceOf()); + var config = (CacheConfig) result; + Assert.That(config.Properties, Is.Not.Null, "Properties"); + Assert.That(config.Properties.Count, Is.EqualTo(0), "Properties count"); + Assert.That(config.Regions, Is.Not.Null, "Regions"); + Assert.That(config.Regions.Length, Is.EqualTo(0)); + } + + [Test] + public void TestGetConfigFromFile() + { + const string xmlSimple = "Value1"; + + var handler = new CoreDistributedCacheSectionHandler(); + var section = GetConfigurationSection(xmlSimple); + var result = handler.Create(null, null, section); + Assert.That(result, Is.Not.Null, "result"); + Assert.That(result, Is.InstanceOf()); + var config = (CacheConfig) result; + + Assert.That(config.FactoryClass, Is.EqualTo("factory1")); + + Assert.That(config.Properties, Is.Not.Null, "Properties"); + Assert.That(config.Properties.Count, Is.EqualTo(1), "Properties count"); + Assert.That(config.Properties, Does.ContainKey("prop1")); + Assert.That(config.Properties["prop1"], Is.EqualTo("Value1")); + + Assert.That(config.Regions, Is.Not.Null, "Regions"); + Assert.That(config.Regions.Length, Is.EqualTo(1), "Regions count"); + Assert.That(config.Regions[0].Region, Is.EqualTo("foo")); + Assert.That(config.Regions[0].Properties, Does.ContainKey("cache.use_sliding_expiration")); + Assert.That(config.Regions[0].Properties["cache.use_sliding_expiration"], Is.EqualTo("true")); + Assert.That(config.Regions[0].Properties, Does.ContainKey("expiration")); + Assert.That(config.Regions[0].Properties["expiration"], Is.EqualTo("500")); + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/DistributedCacheFactoryFixture.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/DistributedCacheFactoryFixture.cs new file mode 100644 index 00000000..adfcecf0 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/DistributedCacheFactoryFixture.cs @@ -0,0 +1,171 @@ +#region License + +// +// CoreDistributedCache - A cache provider for NHibernate using Microsoft.Extensions.Caching.Distributed.IDistributedCache. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// + +#endregion + +using System; +using System.Collections.Generic; +using System.Reflection; +using Enyim.Caching; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.Redis; +using Microsoft.Extensions.Caching.SqlServer; +using NHibernate.Caches.CoreDistributedCache.Memcached; +using NHibernate.Caches.CoreDistributedCache.Memory; +using NHibernate.Caches.CoreDistributedCache.Redis; +using NHibernate.Caches.CoreDistributedCache.SqlServer; +using NUnit.Framework; + +namespace NHibernate.Caches.CoreDistributedCache.Tests +{ + [TestFixture] + public class DistributedCacheFactoryFixture + { + [Test] + public void MemcachedCacheFactory() + { + var factory = + new MemcachedFactory(new Dictionary + { + { + "configuration", @"{ + ""Servers"": [ + { + ""Address"": ""memcached"", + ""Port"": 11211 + } + ], + ""Authentication"": { + ""Type"": ""Enyim.Caching.Memcached.PlainTextAuthenticator"", + ""Parameters"": { + ""zone"": """", + ""userName"": ""username"", + ""password"": ""password"" + } + } +}" + } + }); + var cache1 = factory.BuildCache(); + Assert.That(cache1, Is.Not.Null, "Factory has yielded null"); + Assert.That(cache1, Is.InstanceOf(), "Unexpected cache"); + var cache2 = factory.BuildCache(); + Assert.That(cache2, Is.EqualTo(cache1), + "The Memcached cache factory is supposed to always yield the same instance"); + + var keySanitizer = factory.Constraints?.KeySanitizer; + Assert.That(keySanitizer, Is.Not.Null, "Factory lacks a key sanitizer"); + Assert.That(keySanitizer("--abc \n\r\t\v\fdef--"), Is.EqualTo("--abc------def--"), "Unexpected key sanitization"); + } + + private static readonly FieldInfo MemoryCacheField = + typeof(MemoryDistributedCache).GetField("_memCache", BindingFlags.Instance | BindingFlags.NonPublic); + + private static readonly FieldInfo MemoryCacheOptionsField = + typeof(MemoryCache).GetField("_options", BindingFlags.Instance | BindingFlags.NonPublic); + + [Test] + public void MemoryCacheFactory() + { + var factory = + new MemoryFactory(new Dictionary + { + { "expiration-scan-frequency", "00:10:00" }, + { "size-limit", "1048576" } + }); + Assert.That(factory, Is.Not.Null, "Factory not found"); + Assert.That(factory, Is.InstanceOf(), "Unexpected factory"); + var cache1 = factory.BuildCache(); + Assert.That(cache1, Is.Not.Null, "Factory has yielded null"); + Assert.That(cache1, Is.InstanceOf(), "Unexpected cache"); + var cache2 = factory.BuildCache(); + Assert.That(cache2, Is.EqualTo(cache1), + "The distributed cache factory is supposed to always yield the same instance"); + + var memCache = MemoryCacheField.GetValue(cache1); + Assert.That(memCache, Is.Not.Null, "Underlying memory cache not found"); + Assert.That(memCache, Is.InstanceOf(), "Unexpected memory cache"); + var options = MemoryCacheOptionsField.GetValue(memCache); + Assert.That(options, Is.Not.Null, "Memory cache options not found"); + Assert.That(options, Is.InstanceOf(), "Unexpected options type"); + var memOptions = (MemoryCacheOptions) options; + Assert.That(memOptions.ExpirationScanFrequency, Is.EqualTo(TimeSpan.FromMinutes(10))); + Assert.That(memOptions.SizeLimit, Is.EqualTo(1048576)); + } + + private static readonly FieldInfo RedisCacheOptionsField = + typeof(RedisFactory).GetField("_options", BindingFlags.Instance | BindingFlags.NonPublic); + + [Test] + public void RedisCacheFactory() + { + var factory = + new RedisFactory(new Dictionary + { + { "configuration", "config" }, + { "instance-name", "instance" } + }); + var cache1 = factory.BuildCache(); + Assert.That(cache1, Is.Not.Null, "Factory has yielded null"); + Assert.That(cache1, Is.InstanceOf(), "Unexpected cache"); + var cache2 = factory.BuildCache(); + Assert.That(cache2, Is.Not.EqualTo(cache1), + "The Redis cache factory is supposed to always yield a new instance"); + + var options = RedisCacheOptionsField.GetValue(factory); + Assert.That(options, Is.Not.Null, "Factory cache options not found"); + Assert.That(options, Is.InstanceOf(), "Unexpected options type"); + var redisOptions = (RedisCacheOptions) options; + Assert.That(redisOptions.Configuration, Is.EqualTo("config")); + Assert.That(redisOptions.InstanceName, Is.EqualTo("instance")); + } + + private static readonly FieldInfo SqlServerCacheOptionsField = + typeof(SqlServerFactory).GetField("_options", BindingFlags.Instance | BindingFlags.NonPublic); + + [Test] + public void SqlServerCacheFactory() + { + var factory = new SqlServerFactory(new Dictionary + { + { "connection-string", "connection" }, + { "schema-name", "schema" }, + { "table-name", "table" }, + { "expired-items-deletion-interval", "5" } + }); + var cache1 = factory.BuildCache(); + Assert.That(cache1, Is.Not.Null, "Factory has yielded null"); + Assert.That(cache1, Is.InstanceOf(), "Unexpected cache"); + var cache2 = factory.BuildCache(); + Assert.That(cache2, Is.Not.EqualTo(cache1), + "The SQL Server cache factory is supposed to always yield a new instance"); + + var options = SqlServerCacheOptionsField.GetValue(factory); + Assert.That(options, Is.Not.Null, "Factory cache options not found"); + Assert.That(options, Is.InstanceOf(), "Unexpected options type"); + var sqlServerOptions = (SqlServerCacheOptions) options; + Assert.That(sqlServerOptions.ConnectionString, Is.EqualTo("connection")); + Assert.That(sqlServerOptions.SchemaName, Is.EqualTo("schema")); + Assert.That(sqlServerOptions.TableName, Is.EqualTo("table")); + Assert.That(sqlServerOptions.ExpiredItemsDeletionInterval, Is.EqualTo(TimeSpan.FromMinutes(5))); + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/NHibernate.Caches.CoreDistributedCache.Tests.csproj b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/NHibernate.Caches.CoreDistributedCache.Tests.csproj new file mode 100644 index 00000000..fd88b603 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/NHibernate.Caches.CoreDistributedCache.Tests.csproj @@ -0,0 +1,36 @@ + + + + NHibernate.Caches.CoreDistributedCache + Unit tests of cache provider for NHibernate using .Net Core IDistributedCache (Microsoft.Extensions.Caching.Abstractions). + net461;netcoreapp2.0 + true + + + NETFX;$(DefineConstants) + + + Exe + false + + + + + + + + + + + + + + + + + + + + + + diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/Program.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/Program.cs new file mode 100644 index 00000000..5df1b669 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/Program.cs @@ -0,0 +1,12 @@ +#if !NETFX +namespace NHibernate.Caches.CoreDistributedCache.Tests +{ + public class Program + { + public static int Main(string[] args) + { + return new NUnitLite.AutoRun(typeof(Program).Assembly).Execute(args); + } + } +} +#endif diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/Properties/AssemblyInfo.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..345f41dd --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System; + +[assembly: CLSCompliant(false)] \ No newline at end of file diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/TestsContext.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/TestsContext.cs new file mode 100644 index 00000000..669478e6 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/TestsContext.cs @@ -0,0 +1,42 @@ +using log4net.Repository.Hierarchy; +#if !NETFX +using NHibernate.Caches.Common.Tests; +#endif +using NUnit.Framework; + +namespace NHibernate.Caches.CoreDistributedCache.Tests +{ + [SetUpFixture] + public class TestsContext + { + [OneTimeSetUp] + public void RunBeforeAnyTests() + { +#if !NETFX + TestsContextHelper.RunBeforeAnyTests(typeof(TestsContext).Assembly, "coredistributedcache"); +#endif + ConfigureLog4Net(); + } + +#if !NETFX + [OneTimeTearDown] + public void RunAfterAnyTests() + { + TestsContextHelper.RunAfterAnyTests(); + } +#endif + + private static void ConfigureLog4Net() + { + var hierarchy = (Hierarchy)log4net.LogManager.GetRepository(typeof(TestsContext).Assembly); + + var consoleAppender = new log4net.Appender.ConsoleAppender + { + Layout = new log4net.Layout.PatternLayout("%d{ABSOLUTE} %-5p %c{1}:%L - %m%n"), + }; + hierarchy.Root.Level = log4net.Core.Level.Info; + hierarchy.Root.AddAppender(consoleAppender); + hierarchy.Configured = true; + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/AssemblyInfo.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/AssemblyInfo.cs new file mode 100644 index 00000000..bd9d30e7 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System; +using System.Reflection; + +[assembly: CLSCompliant(true)] +[assembly: AssemblyDelaySign(false)] \ No newline at end of file diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/Async/CoreDistributedCache.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/Async/CoreDistributedCache.cs new file mode 100644 index 00000000..13eb410a --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/Async/CoreDistributedCache.cs @@ -0,0 +1,174 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System; +using NHibernate.Cache; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Caching.Distributed; +using NHibernate.Util; + +namespace NHibernate.Caches.CoreDistributedCache +{ + using System.Threading.Tasks; + using System.Threading; + public partial class CoreDistributedCache : ICache + { + + /// + public async Task GetAsync(object key, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + if (key == null) + { + return null; + } + + var cacheKey = GetCacheKey(key); + Log.Debug("Fetching object '{0}' from the cache.", cacheKey); + + var cachedData = await (_cache.GetAsync(cacheKey, cancellationToken)).ConfigureAwait(false); + if (cachedData == null) + return null; + + var serializer = new BinaryFormatter(); + using (var stream = new MemoryStream(cachedData)) + { + var entry = serializer.Deserialize(stream) as Tuple; + return Equals(entry?.Item1, key) ? entry.Item2 : null; + } + } + + /// + public Task PutAsync(object key, object value, CancellationToken cancellationToken) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key), "null key not allowed"); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value), "null value not allowed"); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + + byte[] cachedData; + var serializer = new BinaryFormatter(); + using (var stream = new MemoryStream()) + { + var entry = new Tuple(key, value); + serializer.Serialize(stream, entry); + cachedData = stream.ToArray(); + } + + var cacheKey = GetCacheKey(key); + var options = new DistributedCacheEntryOptions(); + if (UseSlidingExpiration) + options.SlidingExpiration = Expiration; + else + options.AbsoluteExpirationRelativeToNow = Expiration; + + Log.Debug("putting item with key: {0}", cacheKey); + return _cache.SetAsync(cacheKey, cachedData, options, cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + public Task RemoveAsync(object key, CancellationToken cancellationToken) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + + var cacheKey = GetCacheKey(key); + Log.Debug("removing item with key: {0}", cacheKey); + return _cache.RemoveAsync(cacheKey, cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + public Task ClearAsync(CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + Clear(); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + public Task LockAsync(object key, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + Lock(key); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + + /// + public Task UnlockAsync(object key, CancellationToken cancellationToken) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + Unlock(key); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CacheConfig.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CacheConfig.cs new file mode 100644 index 00000000..5e3bb103 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CacheConfig.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Caching.Distributed; + +namespace NHibernate.Caches.CoreDistributedCache +{ + /// + /// Cache configuration properties. + /// + public class CacheConfig + { + /// + /// Build a cache configuration. + /// + /// The factory class name to use for getting + /// instances. + /// The cache configuration properties. + /// The configured cache regions. + public CacheConfig(string factoryClass, IDictionary properties, RegionConfig[] regions) + { + FactoryClass = factoryClass; + Regions = regions; + Properties = properties; + } + + /// The factory class name to use for getting + /// instances. + public string FactoryClass { get; } + + /// The configured cache regions. + public RegionConfig[] Regions { get; } + + /// The cache configuration properties. + public IDictionary Properties { get; } + } + + /// + /// Region configuration properties. + /// + public class RegionConfig + { + /// + /// Build a cache region configuration. + /// + /// The configured cache region. + /// The expiration for the region. + /// Whether the expiration should be sliding or not. + public RegionConfig(string region, string expiration, string sliding) + { + Region = region; + Properties = new Dictionary(); + if (!string.IsNullOrEmpty(expiration)) + Properties["expiration"] = expiration; + if (!string.IsNullOrEmpty(sliding)) + Properties["cache.use_sliding_expiration"] = sliding; + } + + /// The region name. + public string Region { get; } + + /// The region configuration properties. + public IDictionary Properties { get; } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CacheConstraints.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CacheConstraints.cs new file mode 100644 index 00000000..daf68994 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CacheConstraints.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.Extensions.Caching.Distributed; + +namespace NHibernate.Caches.CoreDistributedCache +{ + /// + /// Constraints of the implementation. + /// + public class CacheConstraints + { + /// + /// If the underlying implementation has a limit on key size, + /// its maximal size, otherwise. + /// + public int? MaxKeySize { get; set; } + + /// + /// If the underlying implementation has constraints on what a key may contain, + /// a function sanitizing provided key, otherwise. + /// + /// + /// + /// If the sanitization function causes two different keys to be equal after sanitization, additional cache + /// misses may occur. (But yielded cached values will not be mixed: either the expected value or + /// will be yielded.) + /// + /// + /// If is also provided, the provided key will already respect it. The yielded value + /// will not be checked again for its maximal length. + /// + /// + public Func KeySanitizer { get; set; } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCache.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCache.cs new file mode 100644 index 00000000..1c89fcd6 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCache.cs @@ -0,0 +1,308 @@ +#region License + +// +// CoreDistributedCache - A cache provider for NHibernate using Microsoft.Extensions.Caching.Distributed.IDistributedCache. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// + +#endregion + +using System; +using NHibernate.Cache; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Caching.Distributed; +using NHibernate.Util; + +namespace NHibernate.Caches.CoreDistributedCache +{ + /// + /// Pluggable cache implementation using implementations. + /// + public partial class CoreDistributedCache : ICache + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(CoreDistributedCache)); + + private readonly IDistributedCache _cache; + private readonly int? _maxKeySize; + private readonly Func _keySanitizer; + + private static readonly TimeSpan DefaultExpiration = TimeSpan.FromSeconds(300); + private const bool _defaultUseSlidingExpiration = false; + private static readonly string DefaultRegionPrefix = string.Empty; + private const string _cacheKeyPrefix = "NHibernate-Cache:"; + + private string _fullRegion; + private bool _hasWarnedOnHashLength; + + /// + /// Default constructor. + /// + /// The instance to use. + /// Optional constraints of . + /// The region of the cache. + /// Cache configuration properties. + /// + /// There are three (3) configurable parameters taken in : + ///
    + ///
  • expiration (or cache.default_expiration) = number of seconds to wait before expiring each item.
  • + ///
  • cache.use_sliding_expiration = a boolean, true for resetting a cached item expiration each time it is accessed.
  • + ///
  • regionPrefix = a string for prefixing the region name.
  • + ///
+ /// All parameters are optional. The defaults are an expiration of 300 seconds, no sliding expiration and no prefix. + ///
+ /// The "expiration" property could not be parsed. + [CLSCompliant(false)] + public CoreDistributedCache( + IDistributedCache cache, CacheConstraints constraints, string region, IDictionary properties) + { + if (constraints?.MaxKeySize <= 0) + throw new ArgumentException($"{nameof(CacheConstraints.MaxKeySize)} must be null or superior to 1.", + nameof(constraints)); + _cache = cache; + _maxKeySize = constraints?.MaxKeySize; + _keySanitizer = constraints?.KeySanitizer; + RegionName = region; + Configure(properties); + } + + /// + public string RegionName { get; } + + /// + /// The expiration delay applied to cached items. + /// + public TimeSpan Expiration { get; private set; } + + /// + /// Should the expiration delay be sliding? + /// + /// for resetting a cached item expiration each time it is accessed. + public bool UseSlidingExpiration { get; private set; } + + private void Configure(IDictionary props) + { + var regionPrefix = DefaultRegionPrefix; + if (props == null) + { + Log.Warn("Configuring cache with default values"); + Expiration = DefaultExpiration; + UseSlidingExpiration = _defaultUseSlidingExpiration; + } + else + { + Expiration = GetExpiration(props); + UseSlidingExpiration = GetUseSlidingExpiration(props); + regionPrefix = GetRegionPrefix(props); + } + + _fullRegion = regionPrefix + RegionName; + } + + private static string GetRegionPrefix(IDictionary props) + { + if (props.TryGetValue("regionPrefix", out var result)) + { + Log.Debug("new regionPrefix: {0}", result); + } + else + { + result = DefaultRegionPrefix; + Log.Debug("no regionPrefix value given, using defaults"); + } + + return result; + } + + private static TimeSpan GetExpiration(IDictionary props) + { + var result = DefaultExpiration; + if (!props.TryGetValue("expiration", out var expirationString)) + { + props.TryGetValue(Cfg.Environment.CacheDefaultExpiration, out expirationString); + } + + if (expirationString != null) + { + if (int.TryParse(expirationString, out var seconds)) + { + result = TimeSpan.FromSeconds(seconds); + Log.Debug("new expiration value: {0}", seconds); + } + else + { + Log.Error("error parsing expiration value '{0}'", expirationString); + throw new ArgumentException($"could not parse expiration '{expirationString}' as a number of seconds"); + } + } + else + { + Log.Debug("no expiration value given, using defaults"); + } + + return result; + } + + private static bool GetUseSlidingExpiration(IDictionary props) + { + var sliding = PropertiesHelper.GetBoolean("cache.use_sliding_expiration", props, _defaultUseSlidingExpiration); + Log.Debug("Use sliding expiration value: {0}", sliding); + return sliding; + } + + private string GetCacheKey(object key) + { + var keyAsString = string.Concat(_cacheKeyPrefix, _fullRegion, ":", key.ToString(), "@", key.GetHashCode()); + + if (_maxKeySize < keyAsString.Length) + { + Log.Info( + "Computing a hashed key for too long key '{0}'. This may cause collisions resulting into additional cache misses.", + key); + // Hash it for respecting max key size. Collisions will be avoided by storing the actual key along + // the object and comparing it on retrieval. + using (var hasher = new SHA256Managed()) + { + var bytes = Encoding.UTF8.GetBytes(keyAsString); + var computedHash = Convert.ToBase64String(hasher.ComputeHash(bytes)); + if (computedHash.Length <= _maxKeySize) + return computedHash; + + if (!_hasWarnedOnHashLength) + { + // No lock for this field, some redundant logs will be less harm than locking. + _hasWarnedOnHashLength = true; + Log.Warn( + "Hash computed for too long keys are themselves too long. They will be truncated, further " + + "increasing the risk of collision resulting into additional cache misses. Consider using a " + + "cache supporting longer keys. Hash length: {0}; max key size: {1}", + computedHash.Length, _maxKeySize); + } + + keyAsString = computedHash.Substring(0, _maxKeySize.Value); + } + } + + return _keySanitizer != null ? _keySanitizer(keyAsString) : keyAsString; + } + + /// + public object Get(object key) + { + if (key == null) + { + return null; + } + + var cacheKey = GetCacheKey(key); + Log.Debug("Fetching object '{0}' from the cache.", cacheKey); + + var cachedData = _cache.Get(cacheKey); + if (cachedData == null) + return null; + + var serializer = new BinaryFormatter(); + using (var stream = new MemoryStream(cachedData)) + { + var entry = serializer.Deserialize(stream) as Tuple; + return Equals(entry?.Item1, key) ? entry.Item2 : null; + } + } + + /// + public void Put(object key, object value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key), "null key not allowed"); + } + + if (value == null) + { + throw new ArgumentNullException(nameof(value), "null value not allowed"); + } + + byte[] cachedData; + var serializer = new BinaryFormatter(); + using (var stream = new MemoryStream()) + { + var entry = new Tuple(key, value); + serializer.Serialize(stream, entry); + cachedData = stream.ToArray(); + } + + var cacheKey = GetCacheKey(key); + var options = new DistributedCacheEntryOptions(); + if (UseSlidingExpiration) + options.SlidingExpiration = Expiration; + else + options.AbsoluteExpirationRelativeToNow = Expiration; + + Log.Debug("putting item with key: {0}", cacheKey); + _cache.Set(cacheKey, cachedData, options); + } + + /// + public void Remove(object key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var cacheKey = GetCacheKey(key); + Log.Debug("removing item with key: {0}", cacheKey); + _cache.Remove(cacheKey); + } + + /// + public void Clear() + { + // Like IMemoryCache, it does not support Clear. Unlike it, it does neither provides a dependency + // mechanism which would allow to implement it. + Log.Warn($"Clear is not supported by {nameof(IDistributedCache)}, ignoring the call."); + } + + /// + public void Destroy() + { + } + + /// + public void Lock(object key) + { + // Do nothing + } + + /// + public void Unlock(object key) + { + // Do nothing + } + + /// + public long NextTimestamp() + { + return Timestamper.Next(); + } + + /// + public int Timeout => Timestamper.OneMs * 60000; + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCacheProvider.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCacheProvider.cs new file mode 100644 index 00000000..a840b9e9 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCacheProvider.cs @@ -0,0 +1,163 @@ +#region License + +// +// CoreDistributedCache - A cache provider for NHibernate using Microsoft.Extensions.Caching.Distributed.IDistributedCache. +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; either +// version 2.1 of the License, or (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// + +#endregion + +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Text; +using Microsoft.Extensions.Caching.Distributed; +using NHibernate.Cache; +using NHibernate.Util; + +namespace NHibernate.Caches.CoreDistributedCache +{ + /// + /// Cache provider using implementations. + /// + public class CoreDistributedCacheProvider : ICacheProvider + { + private static readonly Dictionary> ConfiguredCachesProperties; + private static readonly INHibernateLogger Log; + private static readonly System.Type[] CacheFactoryCtorWithPropertiesSignature = { typeof(IDictionary) }; + + /// + /// The factory to use for getting + /// instances. By default, its value is initialized from factory-class attribute of the + /// coredistributedcache configuration section. + /// + /// + /// Changes to this property affect only caches built after the change. + /// + [CLSCompliant(false)] + public static IDistributedCacheFactory CacheFactory { get; set; } + + static CoreDistributedCacheProvider() + { + Log = NHibernateLogger.For(typeof(CoreDistributedCacheProvider)); + ConfiguredCachesProperties = new Dictionary>(); + + if (!(ConfigurationManager.GetSection("coredistributedcache") is CacheConfig config)) + return; + + if (!string.IsNullOrEmpty(config.FactoryClass)) + { + try + { + var factoryClass = ReflectHelper.ClassForName(config.FactoryClass); + var ctorWithProperties = factoryClass.GetConstructor(CacheFactoryCtorWithPropertiesSignature); + + CacheFactory = (IDistributedCacheFactory) (ctorWithProperties != null ? + ctorWithProperties.Invoke(new object[] { config.Properties }): + Cfg.Environment.BytecodeProvider.ObjectsFactory.CreateInstance(factoryClass)); + } + catch (Exception e) + { + throw new HibernateException( + $"Could not create the {nameof(IDistributedCacheFactory)} factory from '{config.FactoryClass}'. " + + $"(It must implement {nameof(IDistributedCacheFactory)} and have a constructor accepting a " + + $"{nameof(IDictionary)} or have a parameterless constructor.)", + e); + } + } + + foreach (var cache in config.Regions) + { + ConfiguredCachesProperties.Add(cache.Region, cache.Properties); + } + } + + #region ICacheProvider Members + + /// + public ICache BuildCache(string regionName, IDictionary properties) + { + if (CacheFactory == null) + throw new InvalidOperationException( + $"{nameof(CacheFactory)} is null, cannot build a distributed cache without a cache factory. " + + $"Please provide coredistributedcache configuration section with a factory-class attribute or set" + + $"{nameof(CacheFactory)} before building a session factory."); + + if (regionName == null) + { + regionName = string.Empty; + } + + if (ConfiguredCachesProperties.TryGetValue(regionName, out var configuredProperties) && configuredProperties.Count > 0) + { + if (properties != null) + { + // Duplicate it for not altering the global configuration + properties = new Dictionary(properties); + foreach (var prop in configuredProperties) + { + properties[prop.Key] = prop.Value; + } + } + else + { + properties = configuredProperties; + } + } + + // create cache + if (properties == null) + { + properties = new Dictionary(1); + } + + if (Log.IsDebugEnabled()) + { + var sb = new StringBuilder(); + + foreach (var de in properties) + { + sb.Append("name="); + sb.Append(de.Key); + sb.Append("&value="); + sb.Append(de.Value); + sb.Append(";"); + } + + Log.Debug("building cache with region: {0}, properties: {1}, factory: {2}" , regionName, sb.ToString(), CacheFactory.GetType().FullName); + } + return new CoreDistributedCache(CacheFactory.BuildCache(), CacheFactory.Constraints, regionName, properties); + } + + /// + public long NextTimestamp() + { + return Timestamper.Next(); + } + + /// + public void Start(IDictionary properties) + { + } + + /// + public void Stop() + { + } + + #endregion + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCacheSectionHandler.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCacheSectionHandler.cs new file mode 100644 index 00000000..11e0193b --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/CoreDistributedCacheSectionHandler.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Configuration; +using System.Xml; + +namespace NHibernate.Caches.CoreDistributedCache +{ + /// + /// Configuration file provider. + /// + public class CoreDistributedCacheSectionHandler : IConfigurationSectionHandler + { + private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(CoreDistributedCacheSectionHandler)); + + #region IConfigurationSectionHandler Members + + /// + /// A object. + public object Create(object parent, object configContext, XmlNode section) + { + var caches = new List(); + + var nodes = section.SelectNodes("cache"); + foreach (XmlNode node in nodes) + { + var region = node.Attributes["region"]?.Value; + var expiration = node.Attributes["expiration"]?.Value; + var sliding = node.Attributes["sliding"]?.Value; + if (region != null) + { + caches.Add(new RegionConfig(region, expiration, sliding)); + } + else + { + Log.Warn("Found a cache region node lacking a region name: ignored. Node: {0}", + node.OuterXml); + } + } + + var factoryClass = section.Attributes?["factory-class"]?.Value; + var properties = new Dictionary(); + nodes = section.SelectNodes("properties/property"); + foreach (XmlNode node in nodes) + { + var name = node.Attributes["name"]?.Value; + if (name != null) + { + properties.Add(name, node.InnerText); + } + else + { + Log.Warn("Found a cache property node lacking a name: ignored. Node: {0}", + node.OuterXml); + } + } + + return new CacheConfig(factoryClass, properties, caches.ToArray()); + } + + #endregion + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/IDistributedCacheFactory.cs b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/IDistributedCacheFactory.cs new file mode 100644 index 00000000..be5d522d --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/IDistributedCacheFactory.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.Extensions.Caching.Distributed; + +namespace NHibernate.Caches.CoreDistributedCache +{ + /// + /// Interface for factories building instances. + /// + [CLSCompliant(false)] + public interface IDistributedCacheFactory + { + /// + /// Build a instance. + /// + /// A instance. + IDistributedCache BuildCache(); + + /// + /// If the underlying implementation has specific constraints, + /// its constraints, otherwise. + /// + CacheConstraints Constraints { get; } + } +} diff --git a/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.csproj b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.csproj new file mode 100644 index 00000000..268970f9 --- /dev/null +++ b/CoreDistributedCache/NHibernate.Caches.CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.csproj @@ -0,0 +1,36 @@ + + + + NHibernate.Caches.CoreDistributedCache + NHibernate.Caches.CoreDistributedCache + Cache provider for NHibernate using .Net Core IDistributedCache (Microsoft.Extensions.Caching.Abstractions). +This provider is not bound to a specific implementation and require a cache factory yielding IDistributedCache implementations. + + net461;netstandard2.0 + True + ..\..\NHibernate.Caches.snk + true + * New feature + * #28 - Add a .Net Core DistributedCache + + + NETFX;$(DefineConstants) + + + + + + + + + + + + + ./NHibernate.Caches.readme.md + + + ./NHibernate.Caches.license.txt + + + diff --git a/CoreDistributedCache/default.build b/CoreDistributedCache/default.build new file mode 100644 index 00000000..7c4b32bb --- /dev/null +++ b/CoreDistributedCache/default.build @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CoreMemoryCache/NHibernate.Caches.CoreMemoryCache.Tests/TestsContext.cs b/CoreMemoryCache/NHibernate.Caches.CoreMemoryCache.Tests/TestsContext.cs index 2ad480e8..263aeb9a 100644 --- a/CoreMemoryCache/NHibernate.Caches.CoreMemoryCache.Tests/TestsContext.cs +++ b/CoreMemoryCache/NHibernate.Caches.CoreMemoryCache.Tests/TestsContext.cs @@ -1,74 +1,30 @@ -#if !NETFX +using log4net.Repository.Hierarchy; +#if !NETFX +using NHibernate.Caches.Common.Tests; +#endif using NUnit.Framework; -using System.Configuration; -using System.IO; -using System.Reflection; -using log4net.Repository.Hierarchy; -using NHibernate.Cfg; -using NHibernate.Cfg.ConfigurationSchema; -using Environment = NHibernate.Cfg.Environment; namespace NHibernate.Caches.CoreMemoryCache.Tests { [SetUpFixture] public class TestsContext { - private static readonly bool ExecutingWithVsTest = - Assembly.GetEntryAssembly()?.GetName().Name == "testhost"; - - private static bool _removeTesthostConfig; - [OneTimeSetUp] public void RunBeforeAnyTests() { - //When .NET Core App 2.0 tests run from VS/VSTest the entry assembly is "testhost.dll" - //so we need to explicitly load the configuration - if (ExecutingWithVsTest) - { - Environment.InitializeGlobalProperties(GetTestAssemblyHibernateConfiguration()); - ReadCoreCacheSectionFromTesthostConfig(); - } - +#if !NETFX + TestsContextHelper.RunBeforeAnyTests(typeof(TestsContext).Assembly, "corememorycache"); +#endif ConfigureLog4Net(); } +#if !NETFX [OneTimeTearDown] public void RunAfterAnyTests() { - if (_removeTesthostConfig) - { - File.Delete(GetTesthostConfigPath()); - } - } - - private static void ReadCoreCacheSectionFromTesthostConfig() - { - // For caches section, ConfigurationManager being directly used, the only workaround is to provide - // the configuration with its expected file name... - var assemblyPath = - Path.Combine(TestContext.CurrentContext.TestDirectory, Path.GetFileName(typeof(TestsContext).Assembly.Location)); - var configPath = assemblyPath + ".config"; - // If this copy fails: either testconfig has started having its own file, and this hack can no more be used, - // or a previous test run was interupted before its cleanup (RunAfterAnyTests): go clean it manually. - // Discussion about this mess: https://github.com/dotnet/corefx/issues/22101 - File.Copy(configPath, GetTesthostConfigPath()); - _removeTesthostConfig = true; - ConfigurationManager.RefreshSection("corememorycache"); - } - - private static string GetTesthostConfigPath() - { - return Assembly.GetEntryAssembly().Location + ".config"; - } - - private static IHibernateConfiguration GetTestAssemblyHibernateConfiguration() - { - var assemblyPath = - Path.Combine(TestContext.CurrentContext.TestDirectory, Path.GetFileName(typeof(TestsContext).Assembly.Location)); - var configuration = ConfigurationManager.OpenExeConfiguration(assemblyPath); - var section = configuration.GetSection(CfgXmlHelper.CfgSectionName); - return HibernateConfiguration.FromAppConfig(section.SectionInformation.GetRawXml()); + TestsContextHelper.RunAfterAnyTests(); } +#endif private static void ConfigureLog4Net() { @@ -84,4 +40,3 @@ private static void ConfigureLog4Net() } } } -#endif diff --git a/NHibernate.Caches.Common.Tests/Async/CacheFixture.cs b/NHibernate.Caches.Common.Tests/Async/CacheFixture.cs index 81e5b600..2d46633d 100644 --- a/NHibernate.Caches.Common.Tests/Async/CacheFixture.cs +++ b/NHibernate.Caches.Common.Tests/Async/CacheFixture.cs @@ -64,6 +64,9 @@ public async Task TestRemoveAsync() [Test] public async Task TestClearAsync() { + if (!SupportsClear) + Assert.Ignore("Test not supported by provider"); + const string key = "keyTestClear"; const string value = "valueClear"; diff --git a/NHibernate.Caches.Common.Tests/CacheFixture.cs b/NHibernate.Caches.Common.Tests/CacheFixture.cs index 6bc7085c..420da792 100644 --- a/NHibernate.Caches.Common.Tests/CacheFixture.cs +++ b/NHibernate.Caches.Common.Tests/CacheFixture.cs @@ -11,6 +11,7 @@ public abstract partial class CacheFixture : Fixture { protected virtual bool SupportsSlidingExpiration => false; protected virtual bool SupportsDistinguishingKeysWithSameStringRepresentationAndHashcode => true; + protected virtual bool SupportsClear => true; [Test] public void TestPut() @@ -56,6 +57,9 @@ public void TestRemove() [Test] public void TestClear() { + if (!SupportsClear) + Assert.Ignore("Test not supported by provider"); + const string key = "keyTestClear"; const string value = "valueClear"; diff --git a/NHibernate.Caches.Common.Tests/TestsContextHelper.cs b/NHibernate.Caches.Common.Tests/TestsContextHelper.cs new file mode 100644 index 00000000..aa964a9a --- /dev/null +++ b/NHibernate.Caches.Common.Tests/TestsContextHelper.cs @@ -0,0 +1,66 @@ +#if !NETFX +using System.Configuration; +using System.IO; +using System.Reflection; +using NHibernate.Cfg; +using NHibernate.Cfg.ConfigurationSchema; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Caches.Common.Tests +{ + public static class TestsContextHelper + { + public static bool ExecutingWithVsTest { get; } = + Assembly.GetEntryAssembly()?.GetName().Name == "testhost"; + + private static bool _removeTesthostConfig; + + public static void RunBeforeAnyTests(Assembly testAssembly, string configSectionName) + { + //When .NET Core App 2.0 tests run from VS/VSTest the entry assembly is "testhost.dll" + //so we need to explicitly load the configuration + if (ExecutingWithVsTest) + { + var assemblyPath = + Path.Combine(TestContext.CurrentContext.TestDirectory, Path.GetFileName(testAssembly.Location)); + Environment.InitializeGlobalProperties(GetTestAssemblyHibernateConfiguration(assemblyPath)); + ReadCoreCacheSectionFromTesthostConfig(assemblyPath, configSectionName); + } + } + + public static void RunAfterAnyTests() + { + if (_removeTesthostConfig) + { + File.Delete(GetTesthostConfigPath()); + } + } + + private static void ReadCoreCacheSectionFromTesthostConfig(string assemblyPath, string configSectionName) + { + // For caches section, ConfigurationManager being directly used, the only workaround is to provide + // the configuration with its expected file name... + var configPath = assemblyPath + ".config"; + // If this copy fails: either testconfig has started having its own file, and this hack can no more be used, + // or a previous test run was interupted before its cleanup (RunAfterAnyTests): go clean it manually. + // Discussion about this mess: https://github.com/dotnet/corefx/issues/22101 + File.Copy(configPath, GetTesthostConfigPath()); + _removeTesthostConfig = true; + ConfigurationManager.RefreshSection(configSectionName); + } + + private static string GetTesthostConfigPath() + { + return Assembly.GetEntryAssembly().Location + ".config"; + } + + private static IHibernateConfiguration GetTestAssemblyHibernateConfiguration(string assemblyPath) + { + var configuration = ConfigurationManager.OpenExeConfiguration(assemblyPath); + var section = configuration.GetSection(CfgXmlHelper.CfgSectionName); + return HibernateConfiguration.FromAppConfig(section.SectionInformation.GetRawXml()); + } + } +} +#endif diff --git a/NHibernate.Caches.Everything.sln b/NHibernate.Caches.Everything.sln index b12158ae..e49cda03 100644 --- a/NHibernate.Caches.Everything.sln +++ b/NHibernate.Caches.Everything.sln @@ -56,6 +56,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreMemor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreMemoryCache.Tests", "CoreMemoryCache\NHibernate.Caches.CoreMemoryCache.Tests\NHibernate.Caches.CoreMemoryCache.Tests.csproj", "{4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.csproj", "{EFB25D54-38E6-441C-9287-4D69E40B1595}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Tests", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Tests\NHibernate.Caches.CoreDistributedCache.Tests.csproj", "{AD359D7F-6E65-48CB-A59A-B78ED58C1309}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Redis", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Redis\NHibernate.Caches.CoreDistributedCache.Redis.csproj", "{0A6D9315-094E-4C6D-8A87-8C642A2D25D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Memory", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Memory\NHibernate.Caches.CoreDistributedCache.Memory.csproj", "{63AD02AD-1F35-4341-A4C7-7EAEA238F08C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.SqlServer", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.SqlServer\NHibernate.Caches.CoreDistributedCache.SqlServer.csproj", "{03A80D4B-6C72-4F31-8680-DD6119E34CDF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NHibernate.Caches.CoreDistributedCache.Memcached", "CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.Memcached\NHibernate.Caches.CoreDistributedCache.Memcached.csproj", "{CB7BEB99-1964-4D83-86AD-100439E04186}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -138,6 +150,30 @@ Global {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Debug|Any CPU.Build.0 = Debug|Any CPU {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Release|Any CPU.ActiveCfg = Release|Any CPU {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119}.Release|Any CPU.Build.0 = Release|Any CPU + {EFB25D54-38E6-441C-9287-4D69E40B1595}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EFB25D54-38E6-441C-9287-4D69E40B1595}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EFB25D54-38E6-441C-9287-4D69E40B1595}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EFB25D54-38E6-441C-9287-4D69E40B1595}.Release|Any CPU.Build.0 = Release|Any CPU + {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD359D7F-6E65-48CB-A59A-B78ED58C1309}.Release|Any CPU.Build.0 = Release|Any CPU + {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A6D9315-094E-4C6D-8A87-8C642A2D25D7}.Release|Any CPU.Build.0 = Release|Any CPU + {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63AD02AD-1F35-4341-A4C7-7EAEA238F08C}.Release|Any CPU.Build.0 = Release|Any CPU + {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03A80D4B-6C72-4F31-8680-DD6119E34CDF}.Release|Any CPU.Build.0 = Release|Any CPU + {CB7BEB99-1964-4D83-86AD-100439E04186}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB7BEB99-1964-4D83-86AD-100439E04186}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CB7BEB99-1964-4D83-86AD-100439E04186}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CB7BEB99-1964-4D83-86AD-100439E04186}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -162,5 +198,11 @@ Global {D3B6FF9C-4254-48F3-A6A6-64DB03C8A2AC} = {55271617-8CB8-4225-B338-069033160497} {FD759770-E662-4822-A627-85FAA2B3177C} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} {4EE5D89B-A1B5-4CF8-95B8-44C2FAFD0119} = {55271617-8CB8-4225-B338-069033160497} + {EFB25D54-38E6-441C-9287-4D69E40B1595} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {AD359D7F-6E65-48CB-A59A-B78ED58C1309} = {55271617-8CB8-4225-B338-069033160497} + {0A6D9315-094E-4C6D-8A87-8C642A2D25D7} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {63AD02AD-1F35-4341-A4C7-7EAEA238F08C} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {03A80D4B-6C72-4F31-8680-DD6119E34CDF} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} + {CB7BEB99-1964-4D83-86AD-100439E04186} = {9BC335BB-4F31-44B4-9C2D-5A97B4742675} EndGlobalSection EndGlobal diff --git a/appveyor.yml b/appveyor.yml index 5b185a77..72b3b7e3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -99,7 +99,7 @@ test_script: $TestsFailed = $FALSE #netFx tests If ($env:TESTS -eq 'net') { - @('EnyimMemcached', 'Prevalence', 'RtMemoryCache', 'SysCache', 'SysCache2', 'CoreMemoryCache') | ForEach-Object { + @('EnyimMemcached', 'Prevalence', 'RtMemoryCache', 'SysCache', 'SysCache2', 'CoreMemoryCache', 'CoreDistributedCache') | ForEach-Object { nunit3-console (Join-Path $env:APPVEYOR_BUILD_FOLDER "$_\NHibernate.Caches.$_.Tests\bin\$env:CONFIGURATION\$env:NETTARGETFX\NHibernate.Caches.$_.Tests.dll") "--result=$_-NetTestResult.xml;format=AppVeyor" If ($LASTEXITCODE -ne 0) { $TestsFailed = $TRUE @@ -109,8 +109,8 @@ test_script: #core tests If ($env:TESTS -eq 'core') { - @('CoreMemoryCache') | ForEach-Object { - dotnet (Join-Path $env:APPVEYOR_BUILD_FOLDER "$_\NHibernate.Caches.$_.Tests\bin\$env:CONFIGURATION\$env:CORETARGETFX\NHibernate.Caches.$_.Tests.dll") --labels=before --nocolor "--result=$_-CoreTestResult.xml" + @('CoreMemoryCache', 'CoreDistributedCache') | ForEach-Object { + dotnet (Join-Path $env:APPVEYOR_BUILD_FOLDER "$_\NHibernate.Caches.$_.Tests\bin\$env:CONFIGURATION\$env:CORETARGETFX\NHibernate.Caches.$_.Tests.dll") --labels=before --nocolor "--result=$_-CoreTestResult.xml" If ($LASTEXITCODE -ne 0) { $TestsFailed = $TRUE } diff --git a/default.build b/default.build index 0f6761a4..c1724a00 100644 --- a/default.build +++ b/default.build @@ -16,6 +16,7 @@ +