Skip to content

Commit 5f53d02

Browse files
committed
Improve IDistributedCache implementation
1 parent 52e69f8 commit 5f53d02

14 files changed

+526
-95
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace ServiceStackRedisCacheTests
8+
{
9+
[CollectionDefinition(nameof(DistributedCacheCollection))]
10+
public class DistributedCacheCollection : ICollectionFixture<DistributedCacheFixture>
11+
{
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using System.Windows.Markup;
7+
using Microsoft.Extensions.Caching.Distributed;
8+
using Microsoft.Extensions.Configuration;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.DependencyInjection.Extensions;
11+
using Xunit;
12+
using Xunit.Abstractions;
13+
14+
namespace ServiceStackRedisCacheTests;
15+
16+
public class DistributedCacheFixture
17+
{
18+
public IDistributedCache DistributedCache { get; private set; }
19+
20+
public DistributedCacheFixture()
21+
{
22+
using IServiceScope scope = GetServiceProvider().CreateScope();
23+
DistributedCache = scope.ServiceProvider.GetRequiredService<IDistributedCache>();
24+
}
25+
26+
private IServiceProvider GetServiceProvider()
27+
{
28+
IServiceCollection services = new ServiceCollection();
29+
IConfiguration conf = new ConfigurationBuilder().
30+
AddJsonFile("appsettings.json", optional: false)
31+
.Build();
32+
services.AddSingleton(conf);
33+
services.AddLogging();
34+
services.AddEnyimMemcached();
35+
return services.BuildServiceProvider();
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using Enyim.Caching;
2+
using Microsoft.Extensions.Caching.Distributed;
3+
using Xunit.Priority;
4+
5+
namespace ServiceStackRedisCacheTests;
6+
7+
[TestCaseOrderer(PriorityOrderer.Name, PriorityOrderer.Assembly)]
8+
[Collection(nameof(DistributedCacheCollection))]
9+
public class DistributedCacheTests
10+
{
11+
private const string _value = "Coding changes the world";
12+
private readonly IDistributedCache _cache;
13+
14+
public DistributedCacheTests(DistributedCacheFixture fixture)
15+
{
16+
_cache = fixture.DistributedCache;
17+
}
18+
19+
[Fact]
20+
public async Task Cache_with_absolute_expiration()
21+
{
22+
var key = nameof(Cache_with_absolute_expiration) + "_" + Guid.NewGuid();
23+
var keyAsync = nameof(Cache_with_absolute_expiration) + "_async_" + Guid.NewGuid();
24+
25+
var options = new DistributedCacheEntryOptions
26+
{
27+
AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(2)
28+
};
29+
30+
_cache.SetString(key, _value, options);
31+
await _cache.SetStringAsync(keyAsync, _value, options);
32+
33+
Assert.Equal(_value, _cache.GetString(key));
34+
Assert.Equal(_value, await _cache.GetStringAsync(keyAsync));
35+
36+
await Task.Delay(TimeSpan.FromSeconds(3));
37+
38+
Assert.Null(_cache.GetString(key));
39+
Assert.Null(await _cache.GetStringAsync(keyAsync));
40+
}
41+
42+
[Fact]
43+
public async Task Cache_with_relative_to_now()
44+
{
45+
var key = nameof(Cache_with_relative_to_now) + "_" + Guid.NewGuid();
46+
var keyAsync = nameof(Cache_with_relative_to_now) + "_async_" + Guid.NewGuid();
47+
48+
var options = new DistributedCacheEntryOptions
49+
{
50+
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(2)
51+
};
52+
53+
_cache.SetString(key, _value, options);
54+
await _cache.SetStringAsync(keyAsync, _value, options);
55+
56+
Assert.Equal(_value, _cache.GetString(key));
57+
Assert.Equal(_value, await _cache.GetStringAsync(keyAsync));
58+
59+
await Task.Delay(TimeSpan.FromSeconds(3));
60+
61+
Assert.Null(_cache.GetString(key));
62+
Assert.Null(await _cache.GetStringAsync(keyAsync));
63+
}
64+
65+
[Fact]
66+
public async Task Cache_with_sliding_expiration()
67+
{
68+
var key = nameof(Cache_with_sliding_expiration) + "_" + Guid.NewGuid();
69+
var keyAsync = nameof(Cache_with_sliding_expiration) + "_async_" + Guid.NewGuid();
70+
71+
var options = new DistributedCacheEntryOptions
72+
{
73+
SlidingExpiration = TimeSpan.FromSeconds(3)
74+
};
75+
76+
_cache.SetString(key, _value, options);
77+
await _cache.SetStringAsync(keyAsync, _value, options);
78+
79+
Assert.Equal(_value, _cache.GetString(key));
80+
Assert.Equal(_value, await _cache.GetStringAsync(keyAsync));
81+
82+
await Task.Delay(2000);
83+
_cache.Refresh(key);
84+
await _cache.RefreshAsync(keyAsync);
85+
86+
await Task.Delay(2000);
87+
Assert.Equal(_value, _cache.GetString(key));
88+
Assert.Equal(_value, await _cache.GetStringAsync(keyAsync));
89+
90+
await Task.Delay(3100);
91+
Assert.Null(_cache.GetString(key));
92+
Assert.Null(await _cache.GetStringAsync(keyAsync));
93+
}
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net7.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
8+
<IsPackable>false</IsPackable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
13+
<PackageReference Include="xunit" Version="2.4.2" />
14+
<PackageReference Include="Xunit.Priority" Version="1.1.6" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
<PrivateAssets>all</PrivateAssets>
18+
</PackageReference>
19+
<PackageReference Include="coverlet.collector" Version="3.1.2">
20+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
21+
<PrivateAssets>all</PrivateAssets>
22+
</PackageReference>
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<ProjectReference Include="..\Enyim.Caching\Enyim.Caching.csproj" />
27+
</ItemGroup>
28+
29+
<ItemGroup>
30+
<None Update="appsettings.json">
31+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
32+
</None>
33+
</ItemGroup>
34+
35+
</Project>

DistributedCacheTets/Usings.cs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
global using Xunit;

DistributedCacheTets/appsettings.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"enyimMemcached": {
3+
"Servers": [
4+
{
5+
"Address": "memcached",
6+
"Port": 11211
7+
}
8+
]
9+
}
10+
}

Enyim.Caching/DistributedCache.cs

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using Microsoft.Extensions.Caching.Distributed;
2+
using Microsoft.Extensions.Logging;
3+
using System.Threading.Tasks;
4+
using System.Threading;
5+
using Enyim.Caching.Memcached;
6+
using Microsoft.Extensions.Caching.Memory;
7+
using System;
8+
using Microsoft.Extensions.Options;
9+
10+
namespace Enyim.Caching
11+
{
12+
public partial class MemcachedClient
13+
{
14+
#region Implement IDistributedCache
15+
16+
byte[] IDistributedCache.Get(string key)
17+
{
18+
var value = Get<byte[]>(key);
19+
20+
if (value != null)
21+
{
22+
Refresh(key);
23+
}
24+
25+
return value;
26+
}
27+
28+
async Task<byte[]> IDistributedCache.GetAsync(string key, CancellationToken token = default)
29+
{
30+
var value = await GetValueAsync<byte[]>(key);
31+
32+
if (value != null)
33+
{
34+
await RefreshAsync(key);
35+
}
36+
37+
return value;
38+
}
39+
40+
void IDistributedCache.Set(string key, byte[] value, DistributedCacheEntryOptions options)
41+
{
42+
ulong tmp = 0;
43+
var expiration = GetExpiration(options);
44+
PerformStore(StoreMode.Set, key, value, expiration, ref tmp, out var status);
45+
46+
if (options.SlidingExpiration.HasValue)
47+
{
48+
var sldExp = options.SlidingExpiration.Value;
49+
Add(GetSlidingExpirationKey(key), sldExp.ToString(), sldExp);
50+
}
51+
}
52+
53+
async Task IDistributedCache.SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken))
54+
{
55+
var expiration = GetExpiration(options);
56+
await PerformStoreAsync(StoreMode.Set, key, value, expiration);
57+
58+
if (options.SlidingExpiration.HasValue)
59+
{
60+
var sldExp = options.SlidingExpiration.Value;
61+
await AddAsync(GetSlidingExpirationKey(key), sldExp.ToString(), sldExp);
62+
}
63+
}
64+
65+
public void Refresh(string key)
66+
{
67+
var sldExpKey = GetSlidingExpirationKey(key);
68+
var sldExpStr = Get<string>(sldExpKey);
69+
if (!string.IsNullOrEmpty(sldExpStr)
70+
&& TimeSpan.TryParse(sldExpStr, out var sldExp))
71+
{
72+
var value = Get(key);
73+
if (value != null)
74+
{
75+
Replace(key, value, sldExp);
76+
Replace(sldExpKey, sldExpStr, sldExp);
77+
}
78+
}
79+
}
80+
81+
public async Task RefreshAsync(string key, CancellationToken token = default)
82+
{
83+
var sldExpKey = GetSlidingExpirationKey(key);
84+
var sldExpStr = await GetValueAsync<string>(sldExpKey);
85+
if (!string.IsNullOrEmpty(sldExpStr)
86+
&& TimeSpan.TryParse(sldExpStr, out var sldExp))
87+
{
88+
var value = (await GetAsync(key)).Value;
89+
if (value != null)
90+
{
91+
await ReplaceAsync(key, value, sldExp);
92+
await ReplaceAsync(sldExpKey, sldExpStr, sldExp);
93+
}
94+
}
95+
}
96+
97+
void IDistributedCache.Remove(string key)
98+
{
99+
Remove(key);
100+
Remove(GetSlidingExpirationKey(key));
101+
}
102+
103+
async Task IDistributedCache.RemoveAsync(string key, CancellationToken token = default)
104+
{
105+
await RemoveAsync(key);
106+
await RemoveAsync(GetSlidingExpirationKey(key));
107+
}
108+
109+
private uint GetExpiration(DistributedCacheEntryOptions options)
110+
{
111+
if (options.SlidingExpiration.HasValue)
112+
{
113+
return GetExpiration(options.SlidingExpiration);
114+
}
115+
else if (options.AbsoluteExpirationRelativeToNow.HasValue)
116+
{
117+
return GetExpiration(null, relativeToNow: options.AbsoluteExpirationRelativeToNow.Value);
118+
}
119+
else if (options.AbsoluteExpiration.HasValue)
120+
{
121+
return GetExpiration(null, absoluteExpiration: options.AbsoluteExpiration.Value);
122+
}
123+
else
124+
{
125+
throw new ArgumentException("Invalid enum value for options", nameof(options));
126+
}
127+
}
128+
129+
private string GetSlidingExpirationKey(string key) => $"{key}-sliding-expiration";
130+
131+
#endregion
132+
}
133+
}

Enyim.Caching/IMemcachedClient.cs

+12
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,25 @@ namespace Enyim.Caching
99
public interface IMemcachedClient : IDisposable
1010
{
1111
bool Add(string key, object value, int cacheSeconds);
12+
bool Add(string key, object value, uint cacheSeconds);
13+
bool Add(string key, object value, TimeSpan timeSpan);
1214
Task<bool> AddAsync(string key, object value, int cacheSeconds);
15+
Task<bool> AddAsync(string key, object value, uint cacheSeconds);
16+
Task<bool> AddAsync(string key, object value, TimeSpan timeSpan);
1317

1418
bool Set(string key, object value, int cacheSeconds);
19+
bool Set(string key, object value, uint cacheSeconds);
20+
bool Set(string key, object value, TimeSpan timeSpan);
1521
Task<bool> SetAsync(string key, object value, int cacheSeconds);
22+
Task<bool> SetAsync(string key, object value, uint cacheSeconds);
23+
Task<bool> SetAsync(string key, object value, TimeSpan timeSpan);
1624

1725
bool Replace(string key, object value, int cacheSeconds);
26+
bool Replace(string key, object value, uint cacheSeconds);
27+
bool Replace(string key, object value, TimeSpan timeSpan);
1828
Task<bool> ReplaceAsync(string key, object value, int cacheSeconds);
29+
Task<bool> ReplaceAsync(string key, object value, uint cacheSeconds);
30+
Task<bool> ReplaceAsync(string key, object value, TimeSpan timeSpan);
1931

2032
Task<IGetOperationResult> GetAsync(string key);
2133
Task<IGetOperationResult<T>> GetAsync<T>(string key);

0 commit comments

Comments
 (0)