diff --git a/src/Caching/StackExchangeRedis/src/RedisCache.cs b/src/Caching/StackExchangeRedis/src/RedisCache.cs index 4ecbf3628222..95bba7dd0088 100644 --- a/src/Caching/StackExchangeRedis/src/RedisCache.cs +++ b/src/Caching/StackExchangeRedis/src/RedisCache.cs @@ -53,7 +53,7 @@ private static RedisValue[] GetHashFields(bool getData) => getData private long _firstErrorTimeTicks; private long _previousErrorTimeTicks; - internal bool HybridCacheActive { get; set; } + internal virtual bool IsHybridCacheActive() => false; // StackExchange.Redis will also be trying to reconnect internally, // so limit how often we recreate the ConnectionMultiplexer instance @@ -378,7 +378,7 @@ private void TryAddSuffix(IConnectionMultiplexer connection) connection.AddLibraryNameSuffix("aspnet"); connection.AddLibraryNameSuffix("DC"); - if (HybridCacheActive) + if (IsHybridCacheActive()) { connection.AddLibraryNameSuffix("HC"); } diff --git a/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs b/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs index 67d262002eb3..0a58d43e136d 100644 --- a/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs +++ b/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs @@ -11,19 +11,20 @@ namespace Microsoft.Extensions.Caching.StackExchangeRedis; internal sealed class RedisCacheImpl : RedisCache { + private readonly IServiceProvider _services; + + internal override bool IsHybridCacheActive() + => _services.GetService() is not null; + public RedisCacheImpl(IOptions optionsAccessor, ILogger logger, IServiceProvider services) : base(optionsAccessor, logger) { - HybridCacheActive = IsHybridCacheDefined(services); + _services = services; // important: do not check for HybridCache here due to dependency - creates a cycle } public RedisCacheImpl(IOptions optionsAccessor, IServiceProvider services) : base(optionsAccessor) { - HybridCacheActive = IsHybridCacheDefined(services); + _services = services; // important: do not check for HybridCache here due to dependency - creates a cycle } - - // HybridCache optionally uses IDistributedCache; if we're here, then *we are* the DC - private static bool IsHybridCacheDefined(IServiceProvider services) - => services.GetService() is not null; } diff --git a/src/Caching/StackExchangeRedis/test/CacheServiceExtensionsTests.cs b/src/Caching/StackExchangeRedis/test/CacheServiceExtensionsTests.cs index 1d8ce4c3fd40..71e31d19928a 100644 --- a/src/Caching/StackExchangeRedis/test/CacheServiceExtensionsTests.cs +++ b/src/Caching/StackExchangeRedis/test/CacheServiceExtensionsTests.cs @@ -8,10 +8,12 @@ using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Moq; using Xunit; @@ -142,16 +144,46 @@ public void AddStackExchangeRedisCache_HybridCacheDetected(bool hybridCacheActiv services.AddStackExchangeRedisCache(options => { }); if (hybridCacheActive) { - services.TryAddSingleton(new DummyHybridCache()); + services.AddMemoryCache(); + services.TryAddSingleton(); } using var provider = services.BuildServiceProvider(); var cache = Assert.IsAssignableFrom(provider.GetRequiredService()); - Assert.Equal(hybridCacheActive, cache.HybridCacheActive); + Assert.Equal(hybridCacheActive, cache.IsHybridCacheActive()); } sealed class DummyHybridCache : HybridCache { + // emulate the layout from HybridCache in dotnet/extensions + public DummyHybridCache(IOptions options, IServiceProvider services) + { + if (services is null) + { + throw new ArgumentNullException(nameof(services)); + } + + var l1 = services.GetRequiredService(); + _ = options.Value; + var logger = services.GetService()?.CreateLogger(typeof(HybridCache)) ?? NullLogger.Instance; + // var clock = services.GetService() ?? TimeProvider.System; + var l2 = services.GetService(); // note optional + + // ignore L2 if it is really just the same L1, wrapped + // (note not just an "is" test; if someone has a custom subclass, who knows what it does?) + if (l2 is not null + && l2.GetType() == typeof(MemoryDistributedCache) + && l1.GetType() == typeof(MemoryCache)) + { + l2 = null; + } + + IHybridCacheSerializerFactory[] factories = services.GetServices().ToArray(); + Array.Reverse(factories); + } + + public class HybridCacheOptions { } + public override ValueTask GetOrCreateAsync(string key, TState state, Func> factory, HybridCacheEntryOptions options = null, IEnumerable tags = null, CancellationToken cancellationToken = default) => throw new NotSupportedException();