Skip to content

Commit c3ac1d0

Browse files
committed
Make redis optional
For single server scenarios, redis can be left off
1 parent 602aca1 commit c3ac1d0

4 files changed

Lines changed: 148 additions & 91 deletions

File tree

DigitalRuby.SimpleCache/DigitalRuby.SimpleCache.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<Nullable>enable</Nullable>
77
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
88
<IsPackable>true</IsPackable>
9-
<Version>1.0.1</Version>
9+
<Version>1.0.2</Version>
1010
<Title>Simple Cache</Title>
1111
<Authors>jjxtra</Authors>
1212
<Company>Digital Ruby, LLC</Company>

DigitalRuby.SimpleCache/DistributedCache.cs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,39 @@ public interface IDistributedCache
5959
event Action<string>? KeyChanged;
6060
}
6161

62+
/// <summary>
63+
/// Null distributed cache that no-ops everything
64+
/// </summary>
65+
public sealed class NullDistributedCache : IDistributedCache, IDistributedLockFactory
66+
{
67+
/// <inheritdoc />
68+
public event Action<string>? KeyChanged;
69+
70+
/// <inheritdoc />
71+
public Task DeleteAsync(string key, CancellationToken cancelToken = default)
72+
{
73+
return Task.CompletedTask;
74+
}
75+
76+
/// <inheritdoc />
77+
public Task<DistributedCacheItem> GetAsync(string key, CancellationToken cancelToken = default)
78+
{
79+
return Task.FromResult<DistributedCacheItem>(new DistributedCacheItem());
80+
}
81+
82+
/// <inheritdoc />
83+
public Task SetAsync(string key, DistributedCacheItem item, CancellationToken cancelToken = default)
84+
{
85+
return Task.CompletedTask;
86+
}
87+
88+
/// <inheritdoc />
89+
public Task<IAsyncDisposable?> TryAcquireLockAsync(string key, TimeSpan lockTime, TimeSpan timeout = default)
90+
{
91+
return Task.FromResult<IAsyncDisposable?>(new DistributedMemoryCache.FakeDistributedLock());
92+
}
93+
}
94+
6295
/// <summary>
6396
/// Distributed cache but all in memory (for testing)
6497
/// </summary>
@@ -68,7 +101,7 @@ public sealed class DistributedMemoryCache : IDistributedCache, IDistributedLock
68101

69102
public DistributedMemoryCache(ISystemClock clock) => this.clock = clock;
70103

71-
private sealed class FakeDistributedLock : IAsyncDisposable
104+
internal sealed class FakeDistributedLock : IAsyncDisposable
72105
{
73106
public ValueTask DisposeAsync()
74107
{

DigitalRuby.SimpleCache/ServicesExtensions.cs

Lines changed: 109 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -61,80 +61,126 @@ public static void AddSimpleCache(this IServiceCollection services, IConfigurati
6161
/// <param name="configuration">Configuration</param>
6262
public static void AddSimpleCache(this IServiceCollection services, SimpleCacheConfiguration configuration)
6363
{
64-
// assign configuration defaults
6564
SetConfigurationDefaults(configuration);
65+
var layerCacheOptions = AddLayerCacheOptions(services, configuration);
66+
AddSystemClock(services);
67+
AddMemoryCache(services, configuration);
68+
AddFileCache(services, configuration);
69+
AddDistributedCache(services, configuration, layerCacheOptions);
70+
services.AddSingleton<ISerializer>(configuration.SerializerObject);
71+
services.AddSingleton<IDiskSpace, DiskSpace>();
72+
services.AddSingleton<ILayeredCache, LayeredCache>();
73+
}
74+
75+
private static void SetConfigurationDefaults(SimpleCacheConfiguration configuration)
76+
{
77+
if (!string.IsNullOrWhiteSpace(configuration.SerializerType))
78+
{
79+
var serializerType = Type.GetType(configuration.SerializerType) ??
80+
throw new ArgumentException("Invalid serializer type " + configuration.SerializerType);
81+
var serializer = Activator.CreateInstance(serializerType);
82+
if (serializer is ISerializer serializerInterface)
83+
{
84+
configuration.SerializerObject = serializerInterface;
85+
}
86+
else
87+
{
88+
throw new ArgumentException("Failed to detect serializer interface from type " + configuration.SerializerType);
89+
}
90+
}
91+
configuration.SerializerObject ??= new JsonLZ4Serializer();
92+
}
6693

94+
private static LayeredCacheOptions AddLayerCacheOptions(IServiceCollection services, SimpleCacheConfiguration configuration)
95+
{
6796
// add layer cache options
6897
var layerCacheOptions = new LayeredCacheOptions
6998
{
7099
KeyPrefix = configuration.KeyPrefix
71100
};
72101
services.AddSingleton(layerCacheOptions);
102+
return layerCacheOptions;
103+
}
73104

74-
// add file cache options
75-
var fileOptions = new FileCacheOptions
76-
{
77-
CacheDirectory = configuration.FileCacheDirectory,
78-
FreeSpaceThreshold = configuration.FileCacheFreeSpaceThreshold
79-
};
80-
services.AddSingleton(fileOptions);
81-
82-
// add redis cache options
83-
var redisOptions = new DistributedRedisCacheOptions
84-
{
85-
KeyPrefix = layerCacheOptions.KeyPrefix
86-
};
87-
services.AddSingleton(redisOptions);
88-
89-
// a little hacky because of poor api design around AddStackExchangeRedisCache not exposing IServiceProvider
90-
Resolver resolver = new();
91-
services.AddSingleton(resolver);
92-
services.AddHostedService<SimpleCacheHelperService>(); // transfers IServiceProvider to resolver
93-
94-
// setup stackexchange redis
95-
var stackExchangeRedisOptions = ConfigurationOptions.Parse(configuration.RedisConnectionString);
96-
stackExchangeRedisOptions.AbortOnConnectFail = false; // can connect later if initial connection fails
97-
105+
private static void AddSystemClock(IServiceCollection services)
106+
{
98107
// add our own system clock
99108
services.AddSingleton<ClockHandler>();
100109
services.AddSingleton<IClockHandler>(provider => provider.GetRequiredService<ClockHandler>());
101110
services.Replace(new ServiceDescriptor(typeof(ISystemClock), provider => provider.GetRequiredService<ClockHandler>(), ServiceLifetime.Singleton));
111+
}
102112

103-
// add stack exchange redis
104-
services.AddSingleton<IConnectionMultiplexer>(provider =>
113+
private static void AddDistributedCache(IServiceCollection services,
114+
SimpleCacheConfiguration configuration,
115+
LayeredCacheOptions layerCacheOptions)
116+
{
117+
if (string.IsNullOrWhiteSpace(configuration.RedisConnectionString))
105118
{
106-
try
119+
// add null distributed cache
120+
services.AddSingleton<IDistributedCache>(new NullDistributedCache());
121+
}
122+
else
123+
{
124+
// add redis cache options
125+
var redisOptions = new DistributedRedisCacheOptions
126+
{
127+
KeyPrefix = layerCacheOptions.KeyPrefix
128+
};
129+
services.AddSingleton(redisOptions);
130+
131+
// a little hacky because of poor api design around AddStackExchangeRedisCache not exposing IServiceProvider
132+
Resolver resolver = new();
133+
services.AddSingleton(resolver);
134+
services.AddHostedService<SimpleCacheHelperService>(); // transfers IServiceProvider to resolver
135+
136+
// setup stackexchange redis
137+
var stackExchangeRedisOptions = ConfigurationOptions.Parse(configuration.RedisConnectionString);
138+
stackExchangeRedisOptions.AbortOnConnectFail = false; // can connect later if initial connection fails
139+
services.AddSingleton<IConnectionMultiplexer>(provider =>
107140
{
108-
stackExchangeRedisOptions.AllowAdmin = true;
109-
using var admin = ConnectionMultiplexer.Connect(stackExchangeRedisOptions);
110-
var existing = admin.GetServer(admin.GetEndPoints().Single()).ConfigGet("notify-keyspace-events");
111-
if (existing is null ||
112-
existing.Length == 0 ||
113-
!existing.Any(kv =>
141+
try
142+
{
143+
stackExchangeRedisOptions.AllowAdmin = true;
144+
using var admin = ConnectionMultiplexer.Connect(stackExchangeRedisOptions);
145+
var existing = admin.GetServer(admin.GetEndPoints().Single()).ConfigGet("notify-keyspace-events");
146+
if (existing is null ||
147+
existing.Length == 0 ||
148+
!existing.Any(kv =>
149+
{
150+
// this seems to be a random order
151+
var v = kv.Value ?? string.Empty;
152+
return v.Contains('K', StringComparison.OrdinalIgnoreCase) &&
153+
v.Contains('E', StringComparison.OrdinalIgnoreCase) &&
154+
v.Contains('A', StringComparison.OrdinalIgnoreCase);
155+
}))
114156
{
115-
var v = kv.Value ?? string.Empty;
116-
return v.Contains('K', StringComparison.OrdinalIgnoreCase) &&
117-
v.Contains('E', StringComparison.OrdinalIgnoreCase) &&
118-
v.Contains('A', StringComparison.OrdinalIgnoreCase);
119-
}))
157+
admin.GetServer(admin.GetEndPoints().Single()).ConfigSet("notify-keyspace-events", "KEA");
158+
}
159+
}
160+
catch
120161
{
121-
admin.GetServer(admin.GetEndPoints().Single()).ConfigSet("notify-keyspace-events", "KEA");
162+
var logger = provider.GetRequiredService<ILogger<DistributedRedisCache>>();
163+
const string keySpaceCommand = "CONFIG SET notify-keyspace-events KEA";
164+
logger.LogError($"Connection multiplexer has failed to enable key space events, you must manually run this command on your redis servers: '{keySpaceCommand}'");
122165
}
123-
}
124-
catch
166+
stackExchangeRedisOptions.AllowAdmin = false;
167+
return ConnectionMultiplexer.Connect(stackExchangeRedisOptions);
168+
});
169+
services.AddStackExchangeRedisCache(cfg =>
125170
{
126-
var logger = provider.GetRequiredService<ILogger<DistributedRedisCache>>();
127-
const string keySpaceCommand = "CONFIG SET notify-keyspace-events KEA";
128-
logger.LogError($"Connection multiplexer has failed to enable key space events, you must manually run this command on your redis servers: '{keySpaceCommand}'");
129-
}
130-
stackExchangeRedisOptions.AllowAdmin = false;
131-
return ConnectionMultiplexer.Connect(stackExchangeRedisOptions);
132-
});
133-
services.AddStackExchangeRedisCache(cfg =>
134-
{
135-
cfg.ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(resolver.Provider!.GetRequiredService<IConnectionMultiplexer>());
136-
});
171+
cfg.ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(resolver.Provider!.GetRequiredService<IConnectionMultiplexer>());
172+
});
173+
174+
// add redis cache
175+
services.AddSingleton<DistributedRedisCache>();
176+
services.AddHostedService(provider => provider.GetRequiredService<DistributedRedisCache>());
177+
services.AddSingleton<IDistributedCache>(provider => provider.GetRequiredService<DistributedRedisCache>());
178+
services.AddSingleton<IDistributedLockFactory>(provider => provider.GetRequiredService<DistributedRedisCache>());
179+
}
180+
}
137181

182+
private static void AddMemoryCache(IServiceCollection services, SimpleCacheConfiguration configuration)
183+
{
138184
// add memory cache
139185
// another deficiency in the .NET api here, we need the provider to pull out the correct system clock
140186
// in case it has been replaced
@@ -148,17 +194,21 @@ public static void AddSimpleCache(this IServiceCollection services, SimpleCacheC
148194
Clock = provider.GetRequiredService<ISystemClock>()
149195
})));
150196
services.AddSingleton<IMemoryCache>(provider => provider.GetRequiredService<MemoryCache>());
197+
}
151198

152-
// add serializer
153-
services.AddSingleton<ISerializer>(configuration.SerializerObject);
154-
155-
// add disk space checker
156-
services.AddSingleton<IDiskSpace, DiskSpace>();
157-
199+
private static void AddFileCache(IServiceCollection services, SimpleCacheConfiguration configuration)
200+
{
158201
// add file cache
159202
bool useRealFileCache = !string.IsNullOrWhiteSpace(configuration.FileCacheDirectory);
160203
if (useRealFileCache)
161204
{
205+
// add file cache options
206+
var fileOptions = new FileCacheOptions
207+
{
208+
CacheDirectory = configuration.FileCacheDirectory,
209+
FreeSpaceThreshold = configuration.FileCacheFreeSpaceThreshold
210+
};
211+
services.AddSingleton(fileOptions);
162212
services.AddSingleton<FileCache>();
163213
services.AddHostedService(provider => provider.GetRequiredService<FileCache>());
164214
services.AddSingleton<IFileCache>(provider => provider.GetRequiredService<FileCache>());
@@ -167,34 +217,6 @@ public static void AddSimpleCache(this IServiceCollection services, SimpleCacheC
167217
{
168218
services.AddSingleton<IFileCache>(new NullFileCache());
169219
}
170-
171-
// add redis cache
172-
services.AddSingleton<DistributedRedisCache>();
173-
services.AddHostedService(provider => provider.GetRequiredService<DistributedRedisCache>());
174-
services.AddSingleton<IDistributedCache>(provider => provider.GetRequiredService<DistributedRedisCache>());
175-
services.AddSingleton<IDistributedLockFactory>(provider => provider.GetRequiredService<DistributedRedisCache>());
176-
177-
// finally, add the layered cache interface
178-
services.AddSingleton<ILayeredCache, LayeredCache>();
179-
}
180-
181-
private static void SetConfigurationDefaults(SimpleCacheConfiguration configuration)
182-
{
183-
if (!string.IsNullOrWhiteSpace(configuration.SerializerType))
184-
{
185-
var serializerType = Type.GetType(configuration.SerializerType) ??
186-
throw new ArgumentException("Invalid serializer type " + configuration.SerializerType);
187-
var serializer = Activator.CreateInstance(serializerType);
188-
if (serializer is ISerializer serializerInterface)
189-
{
190-
configuration.SerializerObject = serializerInterface;
191-
}
192-
else
193-
{
194-
throw new ArgumentException("Failed to detect serializer interface from type " + configuration.SerializerType);
195-
}
196-
}
197-
configuration.SerializerObject ??= new JsonLZ4Serializer();
198220
}
199221
}
200222

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ The configuration options are:
4343
/* optional, override max memory size (in megabytes). Default is 1024. */
4444
"MaxMemorySize": 2048,
4545

46-
/* redis connection string, required */
46+
/* optional redis connection string */
4747
"RedisConnectionString": "localhost:6379",
4848

4949
/*
@@ -67,7 +67,9 @@ The configuration options are:
6767

6868
```
6969

70-
Only the `RedisConnectionString` is required. For production usage, you should load this from an environment variable.
70+
If the `RedisConnectionString` is empty, no redis cache will be used, an no key change notifications will be sent, preventing auto purge of cache values that are modified.
71+
72+
For production usage, you should load this from an environment variable.
7173

7274
## Usage
7375

0 commit comments

Comments
 (0)