diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs
index a1d4af464a..b0d7da05a2 100644
--- a/src/Core/Settings/GlobalSettings.cs
+++ b/src/Core/Settings/GlobalSettings.cs
@@ -732,7 +732,7 @@ public class GlobalSettings : IGlobalSettings
public class ExtendedCacheSettings
{
public bool EnableDistributedCache { get; set; } = true;
- public bool UseSharedRedisCache { get; set; } = true;
+ public bool UseSharedDistributedCache { get; set; } = true;
public IConnectionStringSettings Redis { get; set; } = new ConnectionStringSettings();
public TimeSpan Duration { get; set; } = TimeSpan.FromMinutes(30);
public bool IsFailSafeEnabled { get; set; } = true;
diff --git a/src/Core/Utilities/CACHING.md b/src/Core/Utilities/CACHING.md
index d838896cbf..d80e629bdd 100644
--- a/src/Core/Utilities/CACHING.md
+++ b/src/Core/Utilities/CACHING.md
@@ -140,7 +140,7 @@ services.AddExtendedCache("MyFeatureCache", globalSettings, new GlobalSettings.E
// Option 4: Isolated Redis for specialized features
services.AddExtendedCache("SpecializedCache", globalSettings, new GlobalSettings.ExtendedCacheSettings
{
- UseSharedRedisCache = false,
+ UseSharedDistributedCache = false,
Redis = new GlobalSettings.ConnectionStringSettings
{
ConnectionString = "localhost:6379,ssl=false"
diff --git a/src/Core/Utilities/ExtendedCacheServiceCollectionExtensions.cs b/src/Core/Utilities/ExtendedCacheServiceCollectionExtensions.cs
index a928240fd7..f287f64e54 100644
--- a/src/Core/Utilities/ExtendedCacheServiceCollectionExtensions.cs
+++ b/src/Core/Utilities/ExtendedCacheServiceCollectionExtensions.cs
@@ -18,9 +18,12 @@ public static class ExtendedCacheServiceCollectionExtensions
/// Adds a new, named Fusion Cache to the service
/// collection. If an existing cache of the same name is found, it will do nothing.
///
- /// Note: When re-using the existing Redis cache, it is expected to call this method after calling
- /// services.AddDistributedCache(globalSettings)
This ensures that DI correctly finds,
- /// configures, and re-uses all the shared Redis architecture.
+ /// Note: When re-using an existing distributed cache, it is expected to call this method after calling
+ /// services.AddDistributedCache(globalSettings)
This ensures that DI correctly finds
+ /// and re-uses the shared distributed cache infrastructure.
+ ///
+ /// Backplane: Cross-instance cache invalidation is only available when using Redis.
+ /// Non-Redis distributed caches operate with eventual consistency across multiple instances.
///
public static IServiceCollection AddExtendedCache(
this IServiceCollection services,
@@ -72,12 +75,21 @@ public static class ExtendedCacheServiceCollectionExtensions
if (!settings.EnableDistributedCache)
return services;
- if (settings.UseSharedRedisCache)
+ if (settings.UseSharedDistributedCache)
{
- // Using Shared Redis, TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)
-
if (!CoreHelpers.SettingHasValue(globalSettings.DistributedCache.Redis.ConnectionString))
+ {
+ // Using Shared Non-Redis Distributed Cache:
+ // 1. Assume IDistributedCache is already registered (e.g., Cosmos, SQL Server)
+ // 2. Backplane not supported (Redis-only feature, requires pub/sub)
+
+ fusionCacheBuilder
+ .TryWithRegisteredDistributedCache();
+
return services;
+ }
+
+ // Using Shared Redis, TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)
services.TryAddSingleton(sp =>
CreateConnectionMultiplexer(sp, cacheName, globalSettings.DistributedCache.Redis.ConnectionString));
@@ -92,13 +104,13 @@ public static class ExtendedCacheServiceCollectionExtensions
});
services.TryAddSingleton(sp =>
+ {
+ var mux = sp.GetRequiredService();
+ return new RedisBackplane(new RedisBackplaneOptions
{
- var mux = sp.GetRequiredService();
- return new RedisBackplane(new RedisBackplaneOptions
- {
- ConnectionMultiplexerFactory = () => Task.FromResult(mux)
- });
+ ConnectionMultiplexerFactory = () => Task.FromResult(mux)
});
+ });
fusionCacheBuilder
.WithRegisteredDistributedCache()
@@ -107,10 +119,21 @@ public static class ExtendedCacheServiceCollectionExtensions
return services;
}
- // Using keyed Redis / Distributed Cache. Create all pieces as keyed services.
+ // Using keyed Distributed Cache. Create/Reuse all pieces as keyed services.
if (!CoreHelpers.SettingHasValue(settings.Redis.ConnectionString))
+ {
+ // Using Keyed Non-Redis Distributed Cache:
+ // 1. Assume IDistributedCache (e.g., Cosmos, SQL Server) is already registered with cacheName as key
+ // 2. Backplane not supported (Redis-only feature, requires pub/sub)
+
+ fusionCacheBuilder
+ .TryWithRegisteredKeyedDistributedCache(serviceKey: cacheName);
+
return services;
+ }
+
+ // Using Keyed Redis: TryAdd and reuse all pieces (multiplexer, distributed cache and backplane)
services.TryAddKeyedSingleton(
cacheName,
diff --git a/test/Core.Test/Utilities/ExtendedCacheServiceCollectionExtensionsTests.cs b/test/Core.Test/Utilities/ExtendedCacheServiceCollectionExtensionsTests.cs
index 6f7fa4df06..e2cb9d5d52 100644
--- a/test/Core.Test/Utilities/ExtendedCacheServiceCollectionExtensionsTests.cs
+++ b/test/Core.Test/Utilities/ExtendedCacheServiceCollectionExtensionsTests.cs
@@ -7,6 +7,7 @@ using NSubstitute;
using StackExchange.Redis;
using Xunit;
using ZiggyCreatures.Caching.Fusion;
+using ZiggyCreatures.Caching.Fusion.Backplane;
namespace Bit.Core.Test.Utilities;
@@ -167,7 +168,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
var settings = CreateGlobalSettings(new()
{
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" },
- { "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedRedisCache", "true" }
+ { "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedDistributedCache", "true" }
});
// Provide a multiplexer (shared)
@@ -187,7 +188,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
{
var settings = new GlobalSettings.ExtendedCacheSettings
{
- UseSharedRedisCache = false,
+ UseSharedDistributedCache = false,
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "invalid:9999" }
};
@@ -242,7 +243,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
{
var settings = new GlobalSettings.ExtendedCacheSettings
{
- UseSharedRedisCache = false,
+ UseSharedDistributedCache = false,
// No Redis connection string
};
@@ -261,13 +262,13 @@ public class ExtendedCacheServiceCollectionExtensionsTests
var settingsA = new GlobalSettings.ExtendedCacheSettings
{
EnableDistributedCache = true,
- UseSharedRedisCache = false,
+ UseSharedDistributedCache = false,
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
};
var settingsB = new GlobalSettings.ExtendedCacheSettings
{
EnableDistributedCache = true,
- UseSharedRedisCache = false,
+ UseSharedDistributedCache = false,
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6380" }
};
@@ -294,7 +295,7 @@ public class ExtendedCacheServiceCollectionExtensionsTests
var settings = new GlobalSettings.ExtendedCacheSettings
{
- UseSharedRedisCache = false,
+ UseSharedDistributedCache = false,
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
};
@@ -306,6 +307,180 @@ public class ExtendedCacheServiceCollectionExtensionsTests
Assert.Same(existingCache, resolved);
}
+ [Fact]
+ public void AddExtendedCache_SharedNonRedisCache_UsesDistributedCacheWithoutBackplane()
+ {
+ var settings = new GlobalSettings.ExtendedCacheSettings
+ {
+ UseSharedDistributedCache = true,
+ EnableDistributedCache = true,
+ // No Redis.ConnectionString
+ };
+
+ // Register non-Redis distributed cache
+ _services.AddSingleton(Substitute.For());
+
+ _services.AddExtendedCache(_cacheName, _globalSettings, settings);
+
+ using var provider = _services.BuildServiceProvider();
+ var cache = provider.GetRequiredKeyedService(_cacheName);
+
+ Assert.True(cache.HasDistributedCache);
+ Assert.False(cache.HasBackplane); // No backplane for non-Redis
+ }
+
+ [Fact]
+ public void AddExtendedCache_SharedRedisWithMockedMultiplexer_ReusesExistingMultiplexer()
+ {
+ // Override GlobalSettings to include Redis connection string
+ var globalSettings = CreateGlobalSettings(new()
+ {
+ { "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }
+ });
+
+ // Custom settings for this cache
+ var settings = new GlobalSettings.ExtendedCacheSettings
+ {
+ UseSharedDistributedCache = true,
+ EnableDistributedCache = true,
+ };
+
+ // Pre-register mocked multiplexer (simulates AddDistributedCache already called)
+ var mockMultiplexer = Substitute.For();
+ _services.AddSingleton(mockMultiplexer);
+
+ _services.AddExtendedCache(_cacheName, globalSettings, settings);
+
+ using var provider = _services.BuildServiceProvider();
+ var cache = provider.GetRequiredKeyedService(_cacheName);
+
+ Assert.True(cache.HasDistributedCache);
+ Assert.True(cache.HasBackplane);
+
+ // Verify same multiplexer was reused (TryAdd didn't replace it)
+ var resolvedMux = provider.GetRequiredService();
+ Assert.Same(mockMultiplexer, resolvedMux);
+ }
+
+ [Fact]
+ public void AddExtendedCache_KeyedNonRedisCache_UsesKeyedDistributedCacheWithoutBackplane()
+ {
+ var settings = new GlobalSettings.ExtendedCacheSettings
+ {
+ UseSharedDistributedCache = false,
+ EnableDistributedCache = true,
+ // No Redis.ConnectionString
+ };
+
+ // Register keyed non-Redis distributed cache
+ _services.AddKeyedSingleton(_cacheName, Substitute.For());
+
+ _services.AddExtendedCache(_cacheName, _globalSettings, settings);
+
+ using var provider = _services.BuildServiceProvider();
+ var cache = provider.GetRequiredKeyedService(_cacheName);
+
+ Assert.True(cache.HasDistributedCache);
+ Assert.False(cache.HasBackplane);
+ }
+
+ [Fact]
+ public void AddExtendedCache_KeyedRedisWithConnectionString_CreatesIsolatedInfrastructure()
+ {
+ var settings = new GlobalSettings.ExtendedCacheSettings
+ {
+ UseSharedDistributedCache = false,
+ EnableDistributedCache = true,
+ Redis = new GlobalSettings.ConnectionStringSettings
+ {
+ ConnectionString = "localhost:6379"
+ }
+ };
+
+ // Pre-register mocked keyed multiplexer to avoid connection attempt
+ _services.AddKeyedSingleton(_cacheName, Substitute.For());
+
+ _services.AddExtendedCache(_cacheName, _globalSettings, settings);
+
+ using var provider = _services.BuildServiceProvider();
+ var cache = provider.GetRequiredKeyedService(_cacheName);
+
+ Assert.True(cache.HasDistributedCache);
+ Assert.True(cache.HasBackplane);
+
+ // Verify keyed services exist
+ var keyedMux = provider.GetRequiredKeyedService(_cacheName);
+ Assert.NotNull(keyedMux);
+ var keyedRedis = provider.GetRequiredKeyedService(_cacheName);
+ Assert.NotNull(keyedRedis);
+ var keyedBackplane = provider.GetRequiredKeyedService(_cacheName);
+ Assert.NotNull(keyedBackplane);
+ }
+
+ [Fact]
+ public void AddExtendedCache_NoDistributedCacheRegistered_WorksWithMemoryOnly()
+ {
+ var settings = new GlobalSettings.ExtendedCacheSettings
+ {
+ UseSharedDistributedCache = true,
+ EnableDistributedCache = true,
+ // No Redis connection string, no IDistributedCache registered
+ // This is technically a misconfiguration, but we handle it without failing
+ };
+
+ _services.AddExtendedCache(_cacheName, _globalSettings, settings);
+
+ using var provider = _services.BuildServiceProvider();
+ var cache = provider.GetRequiredKeyedService(_cacheName);
+
+ Assert.False(cache.HasDistributedCache);
+ Assert.False(cache.HasBackplane);
+
+ // Verify L1 memory cache still works
+ cache.Set("key", "value");
+ var result = cache.GetOrDefault("key");
+ Assert.Equal("value", result);
+ }
+
+ [Fact]
+ public void AddExtendedCache_MultipleKeyedCachesWithDifferentTypes_EachHasCorrectConfig()
+ {
+ var redisSettings = new GlobalSettings.ExtendedCacheSettings
+ {
+ UseSharedDistributedCache = false,
+ EnableDistributedCache = true,
+ Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" }
+ };
+
+ var nonRedisSettings = new GlobalSettings.ExtendedCacheSettings
+ {
+ UseSharedDistributedCache = false,
+ EnableDistributedCache = true,
+ // No Redis connection string
+ };
+
+ // Setup Cache1 (Redis)
+ _services.AddKeyedSingleton("Cache1", Substitute.For());
+ _services.AddExtendedCache("Cache1", _globalSettings, redisSettings);
+
+ // Setup Cache2 (non-Redis)
+ _services.AddKeyedSingleton("Cache2", Substitute.For());
+ _services.AddExtendedCache("Cache2", _globalSettings, nonRedisSettings);
+
+ using var provider = _services.BuildServiceProvider();
+
+ var cache1 = provider.GetRequiredKeyedService("Cache1");
+ var cache2 = provider.GetRequiredKeyedService("Cache2");
+
+ Assert.True(cache1.HasDistributedCache);
+ Assert.True(cache1.HasBackplane);
+
+ Assert.True(cache2.HasDistributedCache);
+ Assert.False(cache2.HasBackplane);
+
+ Assert.NotSame(cache1, cache2);
+ }
+
private static GlobalSettings CreateGlobalSettings(Dictionary data)
{
var config = new ConfigurationBuilder()