diff --git a/src/Caching/StackExchangeRedis/src/RedisCache.cs b/src/Caching/StackExchangeRedis/src/RedisCache.cs index debec0237040..4ecbf3628222 100644 --- a/src/Caching/StackExchangeRedis/src/RedisCache.cs +++ b/src/Caching/StackExchangeRedis/src/RedisCache.cs @@ -53,6 +53,8 @@ private static RedisValue[] GetHashFields(bool getData) => getData private long _firstErrorTimeTicks; private long _previousErrorTimeTicks; + internal bool HybridCacheActive { get; set; } + // StackExchange.Redis will also be trying to reconnect internally, // so limit how often we recreate the ConnectionMultiplexer instance // in an attempt to reconnect @@ -375,6 +377,11 @@ private void TryAddSuffix(IConnectionMultiplexer connection) { connection.AddLibraryNameSuffix("aspnet"); connection.AddLibraryNameSuffix("DC"); + + if (HybridCacheActive) + { + connection.AddLibraryNameSuffix("HC"); + } } catch (Exception ex) { diff --git a/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs b/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs index dab5bfc8655b..67d262002eb3 100644 --- a/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs +++ b/src/Caching/StackExchangeRedis/src/RedisCacheImpl.cs @@ -1,6 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -8,13 +11,19 @@ namespace Microsoft.Extensions.Caching.StackExchangeRedis; internal sealed class RedisCacheImpl : RedisCache { - public RedisCacheImpl(IOptions optionsAccessor, ILogger logger) + public RedisCacheImpl(IOptions optionsAccessor, ILogger logger, IServiceProvider services) : base(optionsAccessor, logger) { + HybridCacheActive = IsHybridCacheDefined(services); } - public RedisCacheImpl(IOptions optionsAccessor) + public RedisCacheImpl(IOptions optionsAccessor, IServiceProvider services) : base(optionsAccessor) { + HybridCacheActive = IsHybridCacheDefined(services); } + + // 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 29a49a7cec70..1d8ce4c3fd40 100644 --- a/src/Caching/StackExchangeRedis/test/CacheServiceExtensionsTests.cs +++ b/src/Caching/StackExchangeRedis/test/CacheServiceExtensionsTests.cs @@ -1,9 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Moq; @@ -121,4 +127,41 @@ public void AddStackExchangeRedisCache_UsesLoggerFactoryAlreadyRegisteredWithSer loggerFactory.Verify(); } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AddStackExchangeRedisCache_HybridCacheDetected(bool hybridCacheActive) + { + // Arrange + var services = new ServiceCollection(); + + services.AddLogging(); + + // Act + services.AddStackExchangeRedisCache(options => { }); + if (hybridCacheActive) + { + services.TryAddSingleton(new DummyHybridCache()); + } + + using var provider = services.BuildServiceProvider(); + var cache = Assert.IsAssignableFrom(provider.GetRequiredService()); + Assert.Equal(hybridCacheActive, cache.HybridCacheActive); + } + + sealed class DummyHybridCache : HybridCache + { + public override ValueTask GetOrCreateAsync(string key, TState state, Func> factory, HybridCacheEntryOptions options = null, IEnumerable tags = null, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public override ValueTask RemoveAsync(string key, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public override ValueTask RemoveByTagAsync(string tag, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public override ValueTask SetAsync(string key, T value, HybridCacheEntryOptions options = null, IEnumerable tags = null, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + } }