using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Backplane;
using ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson;
namespace Microsoft.Extensions.DependencyInjection;
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 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,
string cacheName,
GlobalSettings globalSettings,
GlobalSettings.ExtendedCacheSettings? settings = null)
{
settings ??= globalSettings.DistributedCache.DefaultExtendedCache;
if (settings is null || string.IsNullOrEmpty(cacheName))
{
return services;
}
// If a cache already exists with this key, do nothing
if (services.Any(s => s.ServiceType == typeof(IFusionCache) &&
s.ServiceKey?.Equals(cacheName) == true))
{
return services;
}
if (services.All(s => s.ServiceType != typeof(FusionCacheSystemTextJsonSerializer)))
{
services.AddFusionCacheSystemTextJsonSerializer();
}
var fusionCacheBuilder = services
.AddFusionCache(cacheName)
.WithCacheKeyPrefix($"{cacheName}:")
.AsKeyedServiceByCacheName()
.WithOptions(opt =>
{
opt.DistributedCacheCircuitBreakerDuration = settings.DistributedCacheCircuitBreakerDuration;
})
.WithDefaultEntryOptions(new FusionCacheEntryOptions
{
Duration = settings.Duration,
IsFailSafeEnabled = settings.IsFailSafeEnabled,
FailSafeMaxDuration = settings.FailSafeMaxDuration,
FailSafeThrottleDuration = settings.FailSafeThrottleDuration,
EagerRefreshThreshold = settings.EagerRefreshThreshold,
FactorySoftTimeout = settings.FactorySoftTimeout,
FactoryHardTimeout = settings.FactoryHardTimeout,
DistributedCacheSoftTimeout = settings.DistributedCacheSoftTimeout,
DistributedCacheHardTimeout = settings.DistributedCacheHardTimeout,
AllowBackgroundDistributedCacheOperations = settings.AllowBackgroundDistributedCacheOperations,
JitterMaxDuration = settings.JitterMaxDuration
})
.WithRegisteredSerializer();
if (!settings.EnableDistributedCache)
return services;
if (settings.UseSharedDistributedCache)
{
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));
services.TryAddSingleton(sp =>
{
var mux = sp.GetRequiredService();
return new RedisCache(new RedisCacheOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
});
});
services.TryAddSingleton(sp =>
{
var mux = sp.GetRequiredService();
return new RedisBackplane(new RedisBackplaneOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
});
});
fusionCacheBuilder
.WithRegisteredDistributedCache()
.WithRegisteredBackplane();
return 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,
(sp, _) => CreateConnectionMultiplexer(sp, cacheName, settings.Redis.ConnectionString)
);
services.TryAddKeyedSingleton(
cacheName,
(sp, _) =>
{
var mux = sp.GetRequiredKeyedService(cacheName);
return new RedisCache(new RedisCacheOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
});
}
);
services.TryAddKeyedSingleton(
cacheName,
(sp, _) =>
{
var mux = sp.GetRequiredKeyedService(cacheName);
return new RedisBackplane(new RedisBackplaneOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult(mux)
});
}
);
fusionCacheBuilder
.WithRegisteredKeyedDistributedCacheByCacheName()
.WithRegisteredKeyedBackplaneByCacheName();
return services;
}
private static ConnectionMultiplexer CreateConnectionMultiplexer(IServiceProvider sp, string cacheName,
string connectionString)
{
try
{
return ConnectionMultiplexer.Connect(connectionString);
}
catch (Exception ex)
{
var logger = sp.GetService();
logger?.LogError(ex, "Failed to connect to Redis for cache {CacheName}", cacheName);
throw;
}
}
}