diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs index 7b87755da7b..d55ac1a4ea1 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheOptions.cs @@ -57,4 +57,9 @@ public class HybridCacheOptions /// should not be visible in metrics systems. /// public bool ReportTagMetrics { get; set; } + + /// + /// Gets or sets the key used to resolve the distributed cache service from the . + /// + public object? DistributedCacheServiceKey { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheServiceExtensions.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheServiceExtensions.cs index d28dc4e47d5..060307026d6 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheServiceExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/HybridCacheServiceExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Hybrid.Internal; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.DependencyInjection; @@ -17,28 +18,111 @@ public static class HybridCacheServiceExtensions /// /// Adds support for multi-tier caching services. /// - /// A builder instance that allows further configuration of the system. + /// The to add the service to. + /// A delegate to run to configure the instance. + /// A builder instance that allows further configuration of the service. public static IHybridCacheBuilder AddHybridCache(this IServiceCollection services, Action setupAction) { _ = Throw.IfNull(setupAction); - _ = AddHybridCache(services); + + var builder = AddHybridCache(services); _ = services.Configure(setupAction); - return new HybridCacheBuilder(services); + + return builder; } /// /// Adds support for multi-tier caching services. /// - /// A builder instance that allows further configuration of the system. + /// The to add the service to. + /// A builder instance that allows further configuration of the service. public static IHybridCacheBuilder AddHybridCache(this IServiceCollection services) { _ = Throw.IfNull(services); + var builder = PrepareServices(services); + + services.TryAddSingleton(); + + return builder; + } + + /// + /// Adds support for multi-tier caching services with a keyed registration. + /// + /// The to add the service to. + /// The key for the service registration. + /// A delegate to run to configure the instance. + /// A builder instance that allows further configuration of the service. + public static IHybridCacheBuilder AddKeyedHybridCache(this IServiceCollection services, object? serviceKey, Action setupAction) => + AddKeyedHybridCache(services, serviceKey, serviceKey?.ToString() ?? Options.Options.DefaultName, setupAction); + + /// + /// Adds support for multi-tier caching services with a keyed registration. + /// + /// The to add the service to. + /// The key for the service registration. + /// The named options name to use for the instance. + /// A delegate to run to configure the instance. + /// A builder instance that allows further configuration of the service. + public static IHybridCacheBuilder AddKeyedHybridCache(this IServiceCollection services, object? serviceKey, string optionsName, Action setupAction) + { + _ = Throw.IfNull(setupAction); + + var builder = AddKeyedHybridCache(services, serviceKey, optionsName); + _ = services.AddOptions(optionsName).Configure(setupAction); + + return builder; + } + + /// + /// Adds support for multi-tier caching services with a keyed registration. + /// + /// The to add the service to. + /// The key for the service registration. + /// A builder instance that allows further configuration of the service. + public static IHybridCacheBuilder AddKeyedHybridCache(this IServiceCollection services, object? serviceKey) => + AddKeyedHybridCache(services, serviceKey, serviceKey?.ToString() ?? Options.Options.DefaultName); + + /// + /// Adds support for multi-tier caching services with a keyed registration. + /// + /// The to add the service to. + /// The key for the service registration. + /// The named options name to use for the instance. + /// A builder instance that allows further configuration of the service. + public static IHybridCacheBuilder AddKeyedHybridCache(this IServiceCollection services, object? serviceKey, string optionsName) + { + _ = Throw.IfNull(optionsName); + + var builder = PrepareServices(services); + _ = services.AddOptions(optionsName); + + _ = services.AddKeyedSingleton(serviceKey, (sp, key) => + { + var optionsService = sp.GetRequiredService>(); + var options = optionsService.Get(optionsName); + + return new DefaultHybridCache(options, sp); + }); + + return builder; + } + + /// + /// Adds the services required for hybrid caching. + /// + /// The to prepare with prerequisites. + /// A builder instance that allows further configuration of the service. + private static HybridCacheBuilder PrepareServices(IServiceCollection services) + { + _ = Throw.IfNull(services); + services.TryAddSingleton(TimeProvider.System); _ = services.AddOptions().AddMemoryCache(); services.TryAddSingleton(); services.TryAddSingleton>(InbuiltTypeSerializer.Instance); services.TryAddSingleton>(InbuiltTypeSerializer.Instance); - services.TryAddSingleton(); + return new HybridCacheBuilder(services); } } diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs index 84de2fe52e8..93e1e5457cb 100644 --- a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs +++ b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Internal/DefaultHybridCache.cs @@ -65,13 +65,23 @@ internal enum CacheFeatures internal bool HasBackendCache => (_features & CacheFeatures.BackendCache) != 0; public DefaultHybridCache(IOptions options, IServiceProvider services) + : this(Throw.IfNull(options).Value, services) + { + } + + public DefaultHybridCache(HybridCacheOptions options, IServiceProvider services) { _services = Throw.IfNull(services); _localCache = services.GetRequiredService(); - _options = options.Value; + _options = options; _logger = services.GetService()?.CreateLogger(typeof(HybridCache)) ?? NullLogger.Instance; _clock = services.GetService() ?? TimeProvider.System; - _backendCache = services.GetService(); // note optional + + // The backend cache service is optional; if not provided, we operate as a pure L1 cache. + // If a service key is provided, the service must be present. + _backendCache = _options.DistributedCacheServiceKey is null + ? services.GetService() + : services.GetRequiredKeyedService(_options.DistributedCacheServiceKey); // 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?) diff --git a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Microsoft.Extensions.Caching.Hybrid.json b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Microsoft.Extensions.Caching.Hybrid.json index 2c1a811b223..10be31168ba 100644 Binary files a/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Microsoft.Extensions.Caching.Hybrid.json and b/src/Libraries/Microsoft.Extensions.Caching.Hybrid/Microsoft.Extensions.Caching.Hybrid.json differ diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/BasicConfig.json b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/BasicConfig.json deleted file mode 100644 index 374114fb1db..00000000000 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/BasicConfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "no_entry_options": { - "MaximumKeyLength": 937 - }, - "with_entry_options": { - "MaximumKeyLength": 937, - "DefaultEntryOptions": { - "LocalCacheExpiration": "00:02:00", - "Flags": "DisableCompression,DisableLocalCacheRead" - } - } -} diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/Microsoft.Extensions.Caching.Hybrid.Tests.csproj b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/Microsoft.Extensions.Caching.Hybrid.Tests.csproj index fb8863cf776..3cd6a56dca5 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/Microsoft.Extensions.Caching.Hybrid.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/Microsoft.Extensions.Caching.Hybrid.Tests.csproj @@ -23,10 +23,4 @@ - - - PreserveNewest - - - diff --git a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ServiceConstructionTests.cs b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ServiceConstructionTests.cs index d66b325e802..6aabe04f693 100644 --- a/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ServiceConstructionTests.cs +++ b/test/Libraries/Microsoft.Extensions.Caching.Hybrid.Tests/ServiceConstructionTests.cs @@ -6,13 +6,15 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Hybrid.Internal; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.SqlServer; +using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; #if NET9_0_OR_GREATER using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Json; #endif #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously @@ -51,12 +53,228 @@ public void CanCreateServiceWithManualOptions() Assert.Null(defaults.LocalCacheExpiration); // wasn't specified } + [Fact] + public void CanCreateServiceWithKeyedDistributedCache() + { + var services = new ServiceCollection(); + services.TryAddKeyedSingleton(typeof(CustomMemoryDistributedCache1)); + services.AddHybridCache(options => options.DistributedCacheServiceKey = typeof(CustomMemoryDistributedCache1)); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybrid = Assert.IsType(provider.GetRequiredService()); + var hybridOptions = hybrid.Options; + + var backend = Assert.IsType(hybrid.BackendCache); + Assert.Same(typeof(CustomMemoryDistributedCache1), hybridOptions.DistributedCacheServiceKey); + Assert.Same(backend, provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1))); + } + + [Fact] + public void ThrowsWhenDistributedCacheKeyNotRegistered() + { + var services = new ServiceCollection(); + services.AddHybridCache(options => options.DistributedCacheServiceKey = typeof(CustomMemoryDistributedCache1)); + using ServiceProvider provider = services.BuildServiceProvider(); + + Assert.Throws(provider.GetRequiredService); + } + + [Fact] + public void ThrowsWhenRegisteredDistributedCacheIsNotKeyed() + { + var services = new ServiceCollection(); + services.AddDistributedMemoryCache(); + services.AddHybridCache(options => options.DistributedCacheServiceKey = typeof(CustomMemoryDistributedCache1)); + using ServiceProvider provider = services.BuildServiceProvider(); + + Assert.Throws(provider.GetRequiredService); + } + + [Fact] + public void CanCreateKeyedHybridCacheServiceWithNullKey() + { + var services = new ServiceCollection(); + services.AddKeyedHybridCache(null); + using ServiceProvider provider = services.BuildServiceProvider(); + + // Resolves using null key registration + Assert.IsType(provider.GetRequiredKeyedService(null)); + + // Resolves as the non-keyed registration + Assert.IsType(provider.GetRequiredService()); + } + + [Fact] + public void CanCreateKeyedServicesWithStringKeys() + { + var services = new ServiceCollection(); + services.AddKeyedHybridCache("one"); + services.AddKeyedHybridCache("two"); + services.AddHybridCache(); + using ServiceProvider provider = services.BuildServiceProvider(); + + Assert.IsType(provider.GetRequiredKeyedService("one")); + Assert.IsType(provider.GetRequiredKeyedService("two")); + Assert.IsType(provider.GetRequiredKeyedService(null)); + Assert.IsType(provider.GetRequiredService()); + } + + [Fact] + public void CanCreateKeyedServicesWithStringKeysAndSetupActions() + { + var services = new ServiceCollection(); + services.AddKeyedHybridCache("one", options => options.MaximumKeyLength = 1); + services.AddKeyedHybridCache("two", options => options.MaximumKeyLength = 2); + services.AddKeyedHybridCache(null, options => options.MaximumKeyLength = 3); + using ServiceProvider provider = services.BuildServiceProvider(); + + var one = Assert.IsType(provider.GetRequiredKeyedService("one")); + Assert.Equal(1, one.Options.MaximumKeyLength); + + var two = Assert.IsType(provider.GetRequiredKeyedService("two")); + Assert.Equal(2, two.Options.MaximumKeyLength); + + var threeKeyed = Assert.IsType(provider.GetRequiredKeyedService(null)); + Assert.Equal(3, threeKeyed.Options.MaximumKeyLength); + + var threeUnkeyed = Assert.IsType(provider.GetRequiredService()); + Assert.Equal(3, threeUnkeyed.Options.MaximumKeyLength); + } + + [Fact] + public void CanCreateKeyedServicesWithTypeKeys() + { + var services = new ServiceCollection(); + services.AddKeyedHybridCache(typeof(string)); + services.AddKeyedHybridCache(typeof(int)); + services.AddHybridCache(); + using ServiceProvider provider = services.BuildServiceProvider(); + + Assert.IsType(provider.GetRequiredKeyedService(typeof(string))); + Assert.IsType(provider.GetRequiredKeyedService(typeof(int))); + Assert.IsType(provider.GetRequiredKeyedService(null)); + Assert.IsType(provider.GetRequiredService()); + } + + [Fact] + public void CanCreateKeyedServicesWithTypeKeysAndSetupActions() + { + var services = new ServiceCollection(); + services.AddKeyedHybridCache(typeof(string), options => options.MaximumKeyLength = 1); + services.AddKeyedHybridCache(typeof(int), options => options.MaximumKeyLength = 2); + services.AddKeyedHybridCache(null, options => options.MaximumKeyLength = 3); + + using ServiceProvider provider = services.BuildServiceProvider(); + var one = Assert.IsType(provider.GetRequiredKeyedService(typeof(string))); + Assert.Equal(1, one.Options.MaximumKeyLength); + + var two = Assert.IsType(provider.GetRequiredKeyedService(typeof(int))); + Assert.Equal(2, two.Options.MaximumKeyLength); + + var threeKeyed = Assert.IsType(provider.GetRequiredKeyedService(null)); + Assert.Equal(3, threeKeyed.Options.MaximumKeyLength); + + var threeUnkeyed = Assert.IsType(provider.GetRequiredService()); + Assert.Equal(3, threeUnkeyed.Options.MaximumKeyLength); + } + + [Fact] + public void CanCreateKeyedServicesWithKeyedDistributedCaches() + { + var services = new ServiceCollection(); + services.TryAddKeyedSingleton(typeof(CustomMemoryDistributedCache1)); + services.TryAddKeyedSingleton(typeof(CustomMemoryDistributedCache2)); + + services.AddKeyedHybridCache("one", options => options.DistributedCacheServiceKey = typeof(CustomMemoryDistributedCache1)); + services.AddKeyedHybridCache("two", options => options.DistributedCacheServiceKey = typeof(CustomMemoryDistributedCache2)); + using ServiceProvider provider = services.BuildServiceProvider(); + + var cacheOne = Assert.IsType(provider.GetRequiredKeyedService("one")); + var cacheOneOptions = cacheOne.Options; + var cacheOneBackend = Assert.IsType(cacheOne.BackendCache); + Assert.Same(typeof(CustomMemoryDistributedCache1), cacheOneOptions.DistributedCacheServiceKey); + Assert.Same(cacheOneBackend, provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1))); + + var cacheTwo = Assert.IsType(provider.GetRequiredKeyedService("two")); + var cacheTwoOptions = cacheTwo.Options; + var cacheTwoBackend = Assert.IsType(cacheTwo.BackendCache); + Assert.Same(typeof(CustomMemoryDistributedCache2), cacheTwoOptions.DistributedCacheServiceKey); + Assert.Same(cacheTwoBackend, provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2))); + } + + [Fact] + public async Task KeyedHybridCaches_ShareLocalMemoryCache() + { + var services = new ServiceCollection(); + services.AddMemoryCache(options => options.SizeLimit = 2); + services.AddSingleton(); + services.AddKeyedHybridCache("hybrid1"); + services.AddKeyedHybridCache("hybrid2"); + services.AddKeyedHybridCache("hybrid3"); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybrid1 = provider.GetRequiredKeyedService("hybrid1"); + var hybrid2 = provider.GetRequiredKeyedService("hybrid2"); + var hybrid3 = provider.GetRequiredKeyedService("hybrid3"); + + await hybrid1.SetAsync("entry1", 1); + await hybrid2.SetAsync("entry2", 2); + await hybrid3.SetAsync("entry3", 3); + + var localCache = provider.GetRequiredService(); + Assert.True(localCache.TryGetValue("entry1", out object? _)); + Assert.True(localCache.TryGetValue("entry2", out object? _)); + + // The third item fails to be cached locally because of the shared local cache size limit + Assert.False(localCache.TryGetValue("entry3", out object? _)); + + // But we can still get it from the hybrid cache (which gets it from the distributed cache) + var actual3 = await hybrid3.GetOrCreateAsync("entry3", ct => + { + Assert.Fail("Should not be called as the item should be found in the distributed cache"); + return new ValueTask(-1); + }); + + Assert.Equal(3, actual3); + } + + [Fact] + public void CanCreateRedisAndSqlServerBackedHybridCaches() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("Redis"); + + services.AddKeyedSingleton("SqlServer", + (sp, key) => new SqlServerCache(new SqlServerCacheOptions + { + ConnectionString = "test", + SchemaName = "test", + TableName = "test" + })); + + services.AddKeyedHybridCache("HybridWithRedis", options => options.DistributedCacheServiceKey = "Redis"); + services.AddKeyedHybridCache("HybridWithSqlServer", options => options.DistributedCacheServiceKey = "SqlServer"); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybridWithRedis = Assert.IsType(provider.GetRequiredKeyedService("HybridWithRedis")); + var hybridWithRedisBackend = Assert.IsType(hybridWithRedis.BackendCache); + Assert.Same(hybridWithRedisBackend, provider.GetRequiredKeyedService("Redis")); + + var hybridWithSqlServer = Assert.IsType(provider.GetRequiredKeyedService("HybridWithSqlServer")); + var hybridWithSqlServerBackend = Assert.IsType(hybridWithSqlServer.BackendCache); + Assert.Same(hybridWithSqlServerBackend, provider.GetRequiredKeyedService("SqlServer")); + } + #if NET9_0_OR_GREATER // for Bind API [Fact] public void CanParseOptions_NoEntryOptions() { - var source = new JsonConfigurationSource { Path = "BasicConfig.json" }; - var configBuilder = new ConfigurationBuilder { Sources = { source } }; + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection([ + new("no_entry_options:MaximumKeyLength", "937") + ]); + var config = configBuilder.Build(); var options = new HybridCacheOptions(); ConfigurationBinder.Bind(config, "no_entry_options", options); @@ -68,8 +286,14 @@ public void CanParseOptions_NoEntryOptions() [Fact] public void CanParseOptions_WithEntryOptions() // in particular, check we can parse the timespan and [Flags] enums { - var source = new JsonConfigurationSource { Path = "BasicConfig.json" }; - var configBuilder = new ConfigurationBuilder { Sources = { source } }; + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection([ + new("with_entry_options:MaximumKeyLength", "937"), + new("with_entry_options:DefaultEntryOptions:Flags", "DisableCompression, DisableLocalCacheRead"), + new("with_entry_options:DefaultEntryOptions:LocalCacheExpiration", "00:02:00") + ]); + var config = configBuilder.Build(); var options = new HybridCacheOptions(); ConfigurationBinder.Bind(config, "with_entry_options", options); @@ -81,6 +305,122 @@ public void CanParseOptions_WithEntryOptions() // in particular, check we can pa Assert.Equal(TimeSpan.FromSeconds(120), defaults.LocalCacheExpiration); Assert.Null(defaults.Expiration); // wasn't specified } + + [Fact] + public void CanCreateKeyedServicesWithKeyedDistributedCaches_UsingNamedOptions() + { + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection([ + new("HybridOne:DistributedCacheServiceKey", "DistributedOne"), + new("HybridTwo:DistributedCacheServiceKey", "DistributedTwo") + ]); + + var config = configBuilder.Build(); + + var services = new ServiceCollection(); + services.AddKeyedSingleton("DistributedOne"); + services.AddKeyedSingleton("DistributedTwo"); + services.AddOptions("HybridOne").Configure(options => ConfigurationBinder.Bind(config, "HybridOne", options)); + services.AddOptions("HybridTwo").Configure(options => ConfigurationBinder.Bind(config, "HybridTwo", options)); + services.AddKeyedHybridCache(typeof(CustomMemoryDistributedCache1), "HybridOne"); + services.AddKeyedHybridCache(typeof(CustomMemoryDistributedCache2), "HybridTwo"); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybridOne = Assert.IsType(provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1))); + var hybridOneOptions = hybridOne.Options; + var hybridOneBackend = Assert.IsType(hybridOne.BackendCache); + Assert.Equal("DistributedOne", hybridOneOptions.DistributedCacheServiceKey); + + var hybridTwo = Assert.IsType(provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2))); + var hybridTwoOptions = hybridTwo.Options; + var hybridTwoBackend = Assert.IsType(hybridTwo.BackendCache); + Assert.Equal("DistributedTwo", hybridTwoOptions.DistributedCacheServiceKey); + + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + } + + [Fact] + public void CanCreateKeyedServicesWithKeyedDistributedCaches_UsingSetupActions() + { + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection([ + new("HybridOne:DistributedCacheServiceKey", "DistributedOne"), + new("HybridTwo:DistributedCacheServiceKey", "DistributedTwo") + ]); + + var config = configBuilder.Build(); + + var services = new ServiceCollection(); + services.AddKeyedSingleton("DistributedOne"); + services.AddKeyedSingleton("DistributedTwo"); + services.AddKeyedHybridCache("HybridOne", options => ConfigurationBinder.Bind(config, "HybridOne", options)); + services.AddKeyedHybridCache("HybridTwo", options => ConfigurationBinder.Bind(config, "HybridTwo", options)); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybridOne = Assert.IsType(provider.GetRequiredKeyedService("HybridOne")); + var hybridOneOptions = hybridOne.Options; + var hybridOneBackend = Assert.IsType(hybridOne.BackendCache); + Assert.Equal("DistributedOne", hybridOneOptions.DistributedCacheServiceKey); + + var hybridTwo = Assert.IsType(provider.GetRequiredKeyedService("HybridTwo")); + var hybridTwoOptions = hybridTwo.Options; + var hybridTwoBackend = Assert.IsType(hybridTwo.BackendCache); + Assert.Equal("DistributedTwo", hybridTwoOptions.DistributedCacheServiceKey); + + provider.GetRequiredKeyedService("HybridOne"); + provider.GetRequiredKeyedService("HybridOne"); + provider.GetRequiredKeyedService("HybridOne"); + + provider.GetRequiredKeyedService("HybridTwo"); + provider.GetRequiredKeyedService("HybridTwo"); + provider.GetRequiredKeyedService("HybridTwo"); + } + + [Fact] + public void CanCreateKeyedServicesWithKeyedDistributedCaches_UsingNamedOptionsAndSetupActions() + { + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddInMemoryCollection([ + new("HybridOne:DistributedCacheServiceKey", "DistributedOne"), + new("HybridTwo:DistributedCacheServiceKey", "DistributedTwo") + ]); + + var config = configBuilder.Build(); + + var services = new ServiceCollection(); + services.AddKeyedSingleton("DistributedOne"); + services.AddKeyedSingleton("DistributedTwo"); + services.AddKeyedHybridCache(typeof(CustomMemoryDistributedCache1), "HybridOne", options => ConfigurationBinder.Bind(config, "HybridOne", options)); + services.AddKeyedHybridCache(typeof(CustomMemoryDistributedCache2), "HybridTwo", options => ConfigurationBinder.Bind(config, "HybridTwo", options)); + + using ServiceProvider provider = services.BuildServiceProvider(); + var hybridOne = Assert.IsType(provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1))); + var hybridOneOptions = hybridOne.Options; + var hybridOneBackend = Assert.IsType(hybridOne.BackendCache); + Assert.Equal("DistributedOne", hybridOneOptions.DistributedCacheServiceKey); + + var hybridTwo = Assert.IsType(provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2))); + var hybridTwoOptions = hybridTwo.Options; + var hybridTwoBackend = Assert.IsType(hybridTwo.BackendCache); + Assert.Equal("DistributedTwo", hybridTwoOptions.DistributedCacheServiceKey); + + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache1)); + + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + provider.GetRequiredKeyedService(typeof(CustomMemoryDistributedCache2)); + } #endif [Fact] @@ -173,7 +513,7 @@ public void DefaultMemoryDistributedCacheIsIgnored(bool manual) public void SubclassMemoryDistributedCacheIsNotIgnored() { var services = new ServiceCollection(); - services.AddSingleton(); + services.AddSingleton(); services.AddHybridCache(); using ServiceProvider provider = services.BuildServiceProvider(); var cache = Assert.IsType(provider.GetRequiredService()); @@ -293,14 +633,27 @@ public CustomMemoryCache(IOptions options, ILoggerFactory lo } } - internal class CustomMemoryDistributedCache : MemoryDistributedCache + internal class CustomMemoryDistributedCache1 : MemoryDistributedCache + { + public CustomMemoryDistributedCache1(IOptions options) + : base(options) + { + } + + public CustomMemoryDistributedCache1(IOptions options, ILoggerFactory loggerFactory) + : base(options, loggerFactory) + { + } + } + + internal class CustomMemoryDistributedCache2 : MemoryDistributedCache { - public CustomMemoryDistributedCache(IOptions options) + public CustomMemoryDistributedCache2(IOptions options) : base(options) { } - public CustomMemoryDistributedCache(IOptions options, ILoggerFactory loggerFactory) + public CustomMemoryDistributedCache2(IOptions options, ILoggerFactory loggerFactory) : base(options, loggerFactory) { }