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()