Skip to content

Commit c325393

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

File tree

19 files changed

+441
-76
lines changed

19 files changed

+441
-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: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Text;
4+
using System.Text.RegularExpressions;
5+
using Enyim.Caching;
6+
using Enyim.Caching.Configuration;
7+
using Microsoft.Extensions.Caching.Distributed;
8+
using Microsoft.Extensions.Logging;
9+
using Newtonsoft.Json;
10+
11+
namespace NHibernate.Caches.CoreDistributedCache.Memcached
12+
{
13+
/// <summary>
14+
/// A Memcached distributed cache factory.
15+
/// </summary>
16+
public class MemcachedFactory : IDistributedCacheFactory
17+
{
18+
private static readonly INHibernateLogger Log = NHibernateLogger.For(typeof(MemcachedFactory));
19+
private const string _configuration = "configuration";
20+
21+
private readonly IDistributedCache _cache;
22+
23+
/// <summary>
24+
/// Constructor with explicit configuration properties.
25+
/// </summary>
26+
/// <param name="configuration">The JSON configuration of EnyimMemcachedCore.</param>
27+
/// <remarks><paramref name="configuration"/> has to be structurd like the value part of the
28+
/// <c>"enyimMemcached"</c> property in an appsettings.json file.</remarks>
29+
public MemcachedFactory(string configuration) : this()
30+
{
31+
_cache = CreateCache(configuration);
32+
}
33+
34+
/// <summary>
35+
/// Constructor with configuration properties. It supports <c>configuration</c>, which has to be a JSON string
36+
/// structured like the value part of the <c>"enyimMemcached"</c> property in an appsettings.json file.
37+
/// </summary>
38+
/// <param name="properties">The configurations properties.</param>
39+
public MemcachedFactory(IDictionary<string, string> properties) : this()
40+
{
41+
string configuration = null;
42+
if (properties == null || !properties.TryGetValue(_configuration, out configuration))
43+
{
44+
Log.Warn("No {0} property provided", _configuration);
45+
}
46+
47+
_cache = CreateCache(configuration);
48+
}
49+
50+
private MemcachedFactory()
51+
{
52+
Constraints = new CacheConstraints
53+
{
54+
MaxKeySize = 250,
55+
KeySanitizer = SanitizeKey
56+
};
57+
}
58+
59+
private static IDistributedCache CreateCache(string configuration)
60+
{
61+
var options = string.IsNullOrWhiteSpace(configuration)
62+
? new MemcachedClientOptions()
63+
: JsonConvert.DeserializeObject<MemcachedClientOptions>(configuration);
64+
var loggerFactory = new LoggerFactory();
65+
66+
return new MemcachedClient(loggerFactory, new MemcachedClientConfiguration(loggerFactory, options));
67+
}
68+
69+
// According to https://groups.google.com/forum/#!topic/memcached/Tz1RE0FUbNA,
70+
// memcached key can't contain space, newline, return, tab, vertical tab or form feed.
71+
// Since keys contains entity identifiers which may be anything, purging them all.
72+
// Officially not thread safe, but for just testing Contains, it is actually.
73+
private static readonly HashSet<char> ForbiddenChar = new HashSet<char> { ' ', '\n', '\r', '\t', '\v', '\f' };
74+
private static readonly Regex PurgeForbiddenChar = new Regex("[ \n\r\t\v\f]", RegexOptions.Compiled);
75+
76+
private static string SanitizeKey(string key)
77+
{
78+
foreach (var forbidden in ForbiddenChar)
79+
{
80+
key = key.Replace(forbidden, '-');
81+
}
82+
return key;
83+
84+
/*
85+
var sanitizedKey = new StringBuilder();
86+
foreach (var c in key)
87+
{
88+
sanitizedKey.Append(ForbiddenChar.Contains(c) ? '-' : c);
89+
}
90+
return sanitizedKey.ToString();
91+
*/
92+
93+
//return PurgeForbiddenChar.Replace(key, "-");
94+
}
95+
96+
/// <inheritdoc />
97+
public CacheConstraints Constraints { get; }
98+
99+
/// <inheritdoc />
100+
public IDistributedCache BuildCache()
101+
{
102+
return _cache;
103+
}
104+
105+
private class LoggerFactory : Microsoft.Extensions.Logging.ILoggerFactory
106+
{
107+
public void Dispose()
108+
{
109+
}
110+
111+
public ILogger CreateLogger(string categoryName)
112+
{
113+
return new LoggerWrapper(NHibernateLogger.For(categoryName));
114+
}
115+
116+
public void AddProvider(ILoggerProvider provider)
117+
{
118+
}
119+
}
120+
121+
private class LoggerWrapper : ILogger
122+
{
123+
private readonly INHibernateLogger _logger;
124+
125+
public LoggerWrapper(INHibernateLogger logger)
126+
{
127+
_logger = logger;
128+
}
129+
130+
void ILogger.Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
131+
Func<TState, Exception, string> formatter)
132+
{
133+
if (!IsEnabled(logLevel))
134+
return;
135+
136+
if (formatter == null)
137+
throw new ArgumentNullException(nameof(formatter));
138+
139+
_logger.Log(
140+
TranslateLevel(logLevel),
141+
new NHibernateLogValues("EventId {0}: {1}", new object[] { eventId, formatter(state, exception) }),
142+
// Avoid double logging of exception by not providing it to the logger, but only to the formatter.
143+
null);
144+
}
145+
146+
public bool IsEnabled(LogLevel logLevel)
147+
=> _logger.IsEnabled(TranslateLevel(logLevel));
148+
149+
public IDisposable BeginScope<TState>(TState state)
150+
=> NoopScope.Instance;
151+
152+
private NHibernateLogLevel TranslateLevel(LogLevel level)
153+
{
154+
switch (level)
155+
{
156+
case LogLevel.None:
157+
return NHibernateLogLevel.None;
158+
case LogLevel.Trace:
159+
return NHibernateLogLevel.Trace;
160+
case LogLevel.Debug:
161+
return NHibernateLogLevel.Debug;
162+
case LogLevel.Information:
163+
return NHibernateLogLevel.Info;
164+
case LogLevel.Warning:
165+
return NHibernateLogLevel.Warn;
166+
case LogLevel.Error:
167+
return NHibernateLogLevel.Error;
168+
case LogLevel.Critical:
169+
return NHibernateLogLevel.Fatal;
170+
}
171+
172+
return NHibernateLogLevel.Trace;
173+
}
174+
175+
private class NoopScope : IDisposable
176+
{
177+
public static readonly NoopScope Instance = new NoopScope();
178+
179+
public void Dispose()
180+
{
181+
}
182+
}
183+
}
184+
}
185+
}
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)