Skip to content

Commit 8b5be8b

Browse files
Add a Memcached distributed cache factory
1 parent baf0c87 commit 8b5be8b

File tree

19 files changed

+416
-76
lines changed

19 files changed

+416
-76
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
using System;
2+
using System.Reflection;
3+
4+
[assembly: CLSCompliant(true)]
5+
[assembly: AssemblyDelaySign(false)]
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Enyim.Caching;
4+
using Enyim.Caching.Configuration;
5+
using Microsoft.Extensions.Caching.Distributed;
6+
using Microsoft.Extensions.Logging;
7+
using Newtonsoft.Json;
8+
9+
namespace NHibernate.Caches.CoreDistributedCache.Memcached
10+
{
11+
/// <summary>
12+
/// A Memcached distributed cache factory.
13+
/// </summary>
14+
public class MemcachedFactory : IDistributedCacheFactory
15+
{
16+
private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(MemcachedFactory));
17+
private const string _configuration = "configuration";
18+
19+
private readonly IDistributedCache _cache;
20+
21+
/// <summary>
22+
/// Constructor with configuration properties. It supports <c>configuration</c>, which has to be a JSON string
23+
/// structured like the value part of the <c>"enyimMemcached"</c> property in an appsettings.json file.
24+
/// </summary>
25+
/// <param name="properties">The configurations properties.</param>
26+
public MemcachedFactory(IDictionary<string, string> properties) : this()
27+
{
28+
MemcachedClientOptions options;
29+
if (properties != null && properties.TryGetValue(_configuration, out var configuration) && !string.IsNullOrWhiteSpace(configuration))
30+
{
31+
options = JsonConvert.DeserializeObject<MemcachedClientOptions>(configuration);
32+
}
33+
else
34+
{
35+
Log.Warn("No {0} property provided", _configuration);
36+
options = new MemcachedClientOptions();
37+
}
38+
39+
var loggerFactory = new LoggerFactory();
40+
41+
_cache = new MemcachedClient(loggerFactory, new MemcachedClientConfiguration(loggerFactory, options));
42+
}
43+
44+
private MemcachedFactory()
45+
{
46+
Constraints = new CacheConstraints
47+
{
48+
MaxKeySize = 250,
49+
KeySanitizer = SanitizeKey
50+
};
51+
}
52+
53+
// According to https://groups.google.com/forum/#!topic/memcached/Tz1RE0FUbNA,
54+
// memcached key can't contain space, newline, return, tab, vertical tab or form feed.
55+
// Since keys contains entity identifiers which may be anything, purging them all.
56+
private static readonly char[] ForbiddenChar = new [] { ' ', '\n', '\r', '\t', '\v', '\f' };
57+
58+
private static string SanitizeKey(string key)
59+
{
60+
foreach (var forbidden in ForbiddenChar)
61+
{
62+
key = key.Replace(forbidden, '-');
63+
}
64+
return key;
65+
}
66+
67+
/// <inheritdoc />
68+
public CacheConstraints Constraints { get; }
69+
70+
/// <inheritdoc />
71+
public IDistributedCache BuildCache()
72+
{
73+
return _cache;
74+
}
75+
76+
private class LoggerFactory : Microsoft.Extensions.Logging.ILoggerFactory
77+
{
78+
public void Dispose()
79+
{
80+
}
81+
82+
public ILogger CreateLogger(string categoryName)
83+
{
84+
return new LoggerWrapper(NHibernateLogger.For(categoryName));
85+
}
86+
87+
public void AddProvider(ILoggerProvider provider)
88+
{
89+
}
90+
}
91+
92+
private class LoggerWrapper : ILogger
93+
{
94+
private readonly INHibernateLogger _logger;
95+
96+
public LoggerWrapper(INHibernateLogger logger)
97+
{
98+
_logger = logger;
99+
}
100+
101+
void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
102+
Func<TState, Exception, string> formatter)
103+
{
104+
if (!IsEnabled(logLevel))
105+
return;
106+
107+
if (formatter == null)
108+
throw new ArgumentNullException(nameof(formatter));
109+
110+
_logger.Log(
111+
TranslateLevel(logLevel),
112+
new NHibernateLogValues("EventId {0}: {1}", new object[] { eventId, formatter(state, exception) }),
113+
// Avoid double logging of exception by not providing it to the logger, but only to the formatter.
114+
null);
115+
}
116+
117+
public bool IsEnabled(LogLevel logLevel)
118+
=> _logger.IsEnabled(TranslateLevel(logLevel));
119+
120+
public IDisposable BeginScope<TState>(TState state)
121+
=> NoopScope.Instance;
122+
123+
private NHibernateLogLevel TranslateLevel(LogLevel level)
124+
{
125+
switch (level)
126+
{
127+
case LogLevel.None:
128+
return NHibernateLogLevel.None;
129+
case LogLevel.Trace:
130+
return NHibernateLogLevel.Trace;
131+
case LogLevel.Debug:
132+
return NHibernateLogLevel.Debug;
133+
case LogLevel.Information:
134+
return NHibernateLogLevel.Info;
135+
case LogLevel.Warning:
136+
return NHibernateLogLevel.Warn;
137+
case LogLevel.Error:
138+
return NHibernateLogLevel.Error;
139+
case LogLevel.Critical:
140+
return NHibernateLogLevel.Fatal;
141+
}
142+
143+
return NHibernateLogLevel.Trace;
144+
}
145+
146+
private class NoopScope : IDisposable
147+
{
148+
public static readonly NoopScope Instance = new NoopScope();
149+
150+
public void Dispose()
151+
{
152+
}
153+
}
154+
}
155+
}
156+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="../../NHibernate.Caches.props" />
3+
<PropertyGroup>
4+
<Product>NHibernate.Caches.CoreDistributedCache.Memcached</Product>
5+
<Title>NHibernate.Caches.CoreDistributedCache.Memcached</Title>
6+
<Description>Memcached cache provider for NHibernate using .Net Core IDistributedCache (EnyimMemcachedCore).</Description>
7+
<!-- Targeting net461 explicitly in order to avoid https://github.com/dotnet/standard/issues/506 for net461 consumers-->
8+
<TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
9+
<NoWarn>$(NoWarn);3001;3002</NoWarn>
10+
<SignAssembly>False</SignAssembly>
11+
<PackageReleaseNotes>* New feature
12+
* #28 - Add a .Net Core DistributedCache</PackageReleaseNotes>
13+
</PropertyGroup>
14+
<PropertyGroup Condition="'$(TargetFramework)' == 'net461'">
15+
<DefineConstants>NETFX;$(DefineConstants)</DefineConstants>
16+
</PropertyGroup>
17+
<ItemGroup>
18+
<None Include="..\..\NHibernate.Caches.snk" Link="NHibernate.snk" />
19+
</ItemGroup>
20+
<ItemGroup>
21+
<ProjectReference Include="..\NHibernate.Caches.CoreDistributedCache\NHibernate.Caches.CoreDistributedCache.csproj" />
22+
</ItemGroup>
23+
<ItemGroup>
24+
<Content Include="../../readme.md">
25+
<PackagePath>./NHibernate.Caches.readme.md</PackagePath>
26+
</Content>
27+
<Content Include="../../LICENSE.txt">
28+
<PackagePath>./NHibernate.Caches.license.txt</PackagePath>
29+
</Content>
30+
</ItemGroup>
31+
<ItemGroup>
32+
<PackageReference Include="EnyimMemcachedCore" Version="2.1.0" />
33+
</ItemGroup>
34+
</Project>

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Memory/MemoryFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ public IDistributedCache BuildCache()
112112
}
113113

114114
/// <inheritdoc />
115-
public int? MaxKeySize => null;
115+
public CacheConstraints Constraints => null;
116116

117117
private class Options : MemoryDistributedCacheOptions, IOptions<MemoryDistributedCacheOptions>
118118
{

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Redis/RedisFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public RedisFactory(IDictionary<string, string> properties)
6363
}
6464

6565
/// <inheritdoc />
66-
public int? MaxKeySize => null;
66+
public CacheConstraints Constraints => null;
6767

6868
/// <inheritdoc />
6969
public IDistributedCache BuildCache()

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.SqlServer/SqlServerFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public SqlServerFactory(IDictionary<string, string> properties)
9999
}
100100

101101
/// <inheritdoc />
102-
public int? MaxKeySize => 449;
102+
public CacheConstraints Constraints { get; } = new CacheConstraints { MaxKeySize = 449 };
103103

104104
/// <inheritdoc />
105105
public IDistributedCache BuildCache()

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/App.config

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
type="NHibernate.Cfg.ConfigurationSectionHandler, NHibernate" />
77
</configSections>
88

9+
<!-- For testing with the memory "distributed" cache -->
910
<coredistributedcache factory-class="NHibernate.Caches.CoreDistributedCache.Memory.MemoryFactory,NHibernate.Caches.CoreDistributedCache.Memory">
1011
<properties>
1112
<property name="expiration-scan-frequency">00:10:00</property>
@@ -14,6 +15,24 @@
1415
<cache region="foo" expiration="500" sliding="true" />
1516
<cache region="noExplicitExpiration" sliding="true" />
1617
</coredistributedcache>
18+
19+
<!-- For testing Memcached; note that it fails sliding expiration tests: Memcached does not support sliding
20+
expiration and EnyimMemcachedCore converts them to non-sliding expiration instead.
21+
<coredistributedcache factory-class="NHibernate.Caches.CoreDistributedCache.Memcached.MemcachedFactory,NHibernate.Caches.CoreDistributedCache.Memcached">
22+
<properties>
23+
<property name="configuration">{
24+
"Servers": [
25+
{
26+
"Address": "localhost",
27+
"Port": 11211
28+
}
29+
]
30+
}</property>
31+
</properties>
32+
<cache region="foo" expiration="500" sliding="true" />
33+
<cache region="noExplicitExpiration" sliding="true" />
34+
</coredistributedcache> -->
35+
1736
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">
1837
<session-factory>
1938
<property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</property>

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/Async/CoreDistributedCacheFixture.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,23 @@ public async Task MaxKeySizeAsync()
5050
{
5151
var distribCache = Substitute.For<IDistributedCache>();
5252
const int maxLength = 20;
53-
var cache = new CoreDistributedCache(distribCache, maxLength, "foo", new Dictionary<string, string>());
53+
var cache = new CoreDistributedCache(distribCache, new CacheConstraints { MaxKeySize = maxLength }, "foo",
54+
new Dictionary<string, string>());
5455
await (cache.PutAsync(new string('k', maxLength * 2), "test", CancellationToken.None));
5556
await (distribCache.Received().SetAsync(Arg.Is<string>(k => k.Length <= maxLength), Arg.Any<byte[]>(),
5657
Arg.Any<DistributedCacheEntryOptions>()));
5758
}
59+
60+
[Test]
61+
public async Task KeySanitizerAsync()
62+
{
63+
var distribCache = Substitute.For<IDistributedCache>();
64+
Func<string, string> keySanitizer = s => s.Replace('a', 'b');
65+
var cache = new CoreDistributedCache(distribCache, new CacheConstraints { KeySanitizer = keySanitizer }, "foo",
66+
new Dictionary<string, string>());
67+
await (cache.PutAsync("-abc-", "test", CancellationToken.None));
68+
await (distribCache.Received().SetAsync(Arg.Is<string>(k => k.Contains(keySanitizer("-abc-"))), Arg.Any<byte[]>(),
69+
Arg.Any<DistributedCacheEntryOptions>()));
70+
}
5871
}
5972
}

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheFixture.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,23 @@ public void MaxKeySize()
4444
{
4545
var distribCache = Substitute.For<IDistributedCache>();
4646
const int maxLength = 20;
47-
var cache = new CoreDistributedCache(distribCache, maxLength, "foo", new Dictionary<string, string>());
47+
var cache = new CoreDistributedCache(distribCache, new CacheConstraints { MaxKeySize = maxLength }, "foo",
48+
new Dictionary<string, string>());
4849
cache.Put(new string('k', maxLength * 2), "test");
4950
distribCache.Received().Set(Arg.Is<string>(k => k.Length <= maxLength), Arg.Any<byte[]>(),
5051
Arg.Any<DistributedCacheEntryOptions>());
5152
}
53+
54+
[Test]
55+
public void KeySanitizer()
56+
{
57+
var distribCache = Substitute.For<IDistributedCache>();
58+
Func<string, string> keySanitizer = s => s.Replace('a', 'b');
59+
var cache = new CoreDistributedCache(distribCache, new CacheConstraints { KeySanitizer = keySanitizer }, "foo",
60+
new Dictionary<string, string>());
61+
cache.Put("-abc-", "test");
62+
distribCache.Received().Set(Arg.Is<string>(k => k.Contains(keySanitizer("-abc-"))), Arg.Any<byte[]>(),
63+
Arg.Any<DistributedCacheEntryOptions>());
64+
}
5265
}
5366
}

CoreDistributedCache/NHibernate.Caches.CoreDistributedCache.Tests/CoreDistributedCacheProviderFixture.cs

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,8 @@
2222

2323
using System;
2424
using System.Collections.Generic;
25-
using System.Reflection;
26-
using Microsoft.Extensions.Caching.Distributed;
27-
using Microsoft.Extensions.Caching.Memory;
2825
using NHibernate.Cache;
2926
using NHibernate.Caches.Common.Tests;
30-
using NHibernate.Caches.CoreDistributedCache.Memory;
3127
using NUnit.Framework;
3228

3329
namespace NHibernate.Caches.CoreDistributedCache.Tests
@@ -38,36 +34,6 @@ public class CoreDistributedCacheProviderFixture : CacheProviderFixture
3834
protected override Func<ICacheProvider> ProviderBuilder =>
3935
() => new CoreDistributedCacheProvider();
4036

41-
private static readonly FieldInfo MemoryCacheField =
42-
typeof(MemoryDistributedCache).GetField("_memCache", BindingFlags.Instance | BindingFlags.NonPublic);
43-
44-
private static readonly FieldInfo CacheOptionsField =
45-
typeof(MemoryCache).GetField("_options", BindingFlags.Instance | BindingFlags.NonPublic);
46-
47-
[Test]
48-
public void ConfiguredCacheFactory()
49-
{
50-
var factory = CoreDistributedCacheProvider.CacheFactory;
51-
Assert.That(factory, Is.Not.Null, "Factory not found");
52-
Assert.That(factory, Is.InstanceOf<MemoryFactory>(), "Unexpected factory");
53-
var cache1 = factory.BuildCache();
54-
Assert.That(cache1, Is.Not.Null, "Factory has yielded null");
55-
Assert.That(cache1, Is.InstanceOf<MemoryDistributedCache>(), "Unexpected cache");
56-
var cache2 = factory.BuildCache();
57-
Assert.That(cache2, Is.EqualTo(cache1),
58-
"The distributed cache factory is supposed to always yield the same instance");
59-
60-
var memCache = MemoryCacheField.GetValue(cache1);
61-
Assert.That(memCache, Is.Not.Null, "Underlying memory cache not found");
62-
Assert.That(memCache, Is.InstanceOf<MemoryCache>(), "Unexpected memory cache");
63-
var options = CacheOptionsField.GetValue(memCache);
64-
Assert.That(options, Is.Not.Null, "Memory cache options not found");
65-
Assert.That(options, Is.InstanceOf<MemoryCacheOptions>(), "Unexpected options type");
66-
var memOptions = (MemoryCacheOptions) options;
67-
Assert.That(memOptions.ExpirationScanFrequency, Is.EqualTo(TimeSpan.FromMinutes(10)));
68-
Assert.That(memOptions.SizeLimit, Is.EqualTo(1048576));
69-
}
70-
7137
[Test]
7238
public void TestBuildCacheFromConfig()
7339
{

0 commit comments

Comments
 (0)