From ea7d7cd454d677956ebdc1069b7f128e63b7f4f3 Mon Sep 17 00:00:00 2001 From: Benjamin Soddy Date: Thu, 12 Dec 2024 09:16:25 -0600 Subject: [PATCH 01/14] feature: split for hybridcache --- .github/workflows/publish.yml | 4 +- .vscode/launch.json | 20 ++ Directory.Build.props | 12 + Directory.Packages.props | 24 ++ L1L2RedisCache.sln | 86 ++++- .../MessagingRedisCache.Lab.csproj | 19 ++ labs/MessagingRedisCache/Program.cs | 38 +++ .../ServiceCollectionExtensions.cs | 104 ------ src/L1L2RedisCache.csproj | 29 -- .../ServiceCollectionExtensions.cs | 29 ++ .../HashEntryArrayExtensions.cs | 2 +- src/{ => L1L2RedisCache}/L1L2RedisCache.cs | 298 ++++-------------- src/L1L2RedisCache/L1L2RedisCache.csproj | 24 ++ src/L1L2RedisCache/README.md | 41 +++ src/L1L2RedisCacheOptions.cs | 50 --- src/Messaging/DefaultMessagePublisher.cs | 56 ---- src/{ => MessagingRedisCache}/CacheMessage.cs | 2 +- .../IMessagingConfigurationVerifier.cs | 2 +- .../IMessagingRedisCacheBuilder.cs | 11 + .../MessagingConfigurationVerifier.cs | 18 +- .../MessagingRedisCacheBuilder.cs | 11 + .../MessagingRedisCacheBuilderExtensions.cs | 41 +++ .../ServiceCollectionExtensions.cs | 76 +++++ .../Messaging/DefaultMessagePublisher.cs | 53 ++++ .../Messaging/DefaultMessageSubscriber.cs | 33 +- .../Messaging/IMessagePublisher.cs | 6 +- .../Messaging/IMessageSubscriber.cs | 2 +- .../Messaging/KeyeventMessageSubscriber.cs | 28 +- .../Messaging/KeyspaceMessageSubscriber.cs | 45 ++- .../Messaging/NopMessagePublisher.cs | 6 +- .../Messaging/NopMessageSubscriber.cs | 24 ++ .../Messaging/OnMessageEventArgs.cs | 9 +- .../MessagingRedisCache.cs | 228 ++++++++++++++ .../MessagingRedisCache.csproj | 21 ++ .../MessagingRedisCacheLoggerExtensions.cs} | 8 +- .../MessagingRedisCacheOptions.cs | 34 ++ .../MessagingType.cs | 2 +- src/MessagingRedisCache/README.md | 53 ++++ .../SourceGenerationContext.cs | 11 + .../System/Benchmark/BenchmarkBase.cs | 12 +- .../System/Benchmark/GetBenchmark.cs | 2 +- .../System/Benchmark/SetBenchmark.cs | 0 .../System/L1L2RedisCache.Tests.System.csproj | 24 ++ .../System/MessagingTests.cs | 27 +- .../System/PerformanceTests.cs | 14 +- .../System/ReliabilityTests.cs | 4 +- .../System/appsettings.json | 2 +- .../System/DefaultMessagingTests.cs | 15 + .../System/HybridCacheTests.cs | 106 +++++++ .../System/KeyeventMessagingTests.cs | 15 + .../System/KeyspaceMessagingTests.cs | 15 + .../MessagingRedisCache.Tests.System.csproj | 24 ++ .../System/MessagingTestsBase.cs | 176 +++++++++++ tests/MessagingRedisCache/System/TestsBase.cs | 104 ++++++ .../System/appsettings.json | 7 + .../MessagingRedisCache.Tests.Unit.csproj | 17 + .../Unit/MessagingRedisCacheTests.cs | 138 ++++++++ .../System/L1L2RedisCache.Tests.System.csproj | 28 -- tests/Unit/L1L2RedisCache.Tests.Unit.csproj | 20 -- tests/Unit/L1L2RedisCacheTests.cs | 282 ----------------- 60 files changed, 1662 insertions(+), 930 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 Directory.Build.props create mode 100644 Directory.Packages.props create mode 100644 labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj create mode 100644 labs/MessagingRedisCache/Program.cs delete mode 100644 src/Configuration/ServiceCollectionExtensions.cs delete mode 100644 src/L1L2RedisCache.csproj create mode 100644 src/L1L2RedisCache/Configuration/ServiceCollectionExtensions.cs rename src/{ => L1L2RedisCache}/HashEntryArrayExtensions.cs (96%) rename src/{ => L1L2RedisCache}/L1L2RedisCache.cs (55%) create mode 100644 src/L1L2RedisCache/L1L2RedisCache.csproj create mode 100644 src/L1L2RedisCache/README.md delete mode 100644 src/L1L2RedisCacheOptions.cs delete mode 100644 src/Messaging/DefaultMessagePublisher.cs rename src/{ => MessagingRedisCache}/CacheMessage.cs (92%) rename src/{ => MessagingRedisCache}/Configuration/IMessagingConfigurationVerifier.cs (95%) create mode 100644 src/MessagingRedisCache/Configuration/IMessagingRedisCacheBuilder.cs rename src/{ => MessagingRedisCache}/Configuration/MessagingConfigurationVerifier.cs (71%) create mode 100644 src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilder.cs create mode 100644 src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilderExtensions.cs create mode 100644 src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs create mode 100644 src/MessagingRedisCache/Messaging/DefaultMessagePublisher.cs rename src/{ => MessagingRedisCache}/Messaging/DefaultMessageSubscriber.cs (61%) rename src/{ => MessagingRedisCache}/Messaging/IMessagePublisher.cs (94%) rename src/{ => MessagingRedisCache}/Messaging/IMessageSubscriber.cs (98%) rename src/{ => MessagingRedisCache}/Messaging/KeyeventMessageSubscriber.cs (75%) rename src/{ => MessagingRedisCache}/Messaging/KeyspaceMessageSubscriber.cs (56%) rename src/{ => MessagingRedisCache}/Messaging/NopMessagePublisher.cs (82%) create mode 100644 src/MessagingRedisCache/Messaging/NopMessageSubscriber.cs rename src/{ => MessagingRedisCache}/Messaging/OnMessageEventArgs.cs (73%) create mode 100644 src/MessagingRedisCache/MessagingRedisCache.cs create mode 100644 src/MessagingRedisCache/MessagingRedisCache.csproj rename src/{L1L2RedisCacheLoggerExtensions.cs => MessagingRedisCache/MessagingRedisCacheLoggerExtensions.cs} (75%) create mode 100644 src/MessagingRedisCache/MessagingRedisCacheOptions.cs rename src/{ => MessagingRedisCache}/MessagingType.cs (97%) create mode 100644 src/MessagingRedisCache/README.md create mode 100644 src/MessagingRedisCache/SourceGenerationContext.cs rename tests/{ => L1L2RedisCache}/System/Benchmark/BenchmarkBase.cs (84%) rename tests/{ => L1L2RedisCache}/System/Benchmark/GetBenchmark.cs (98%) rename tests/{ => L1L2RedisCache}/System/Benchmark/SetBenchmark.cs (100%) create mode 100644 tests/L1L2RedisCache/System/L1L2RedisCache.Tests.System.csproj rename tests/{ => L1L2RedisCache}/System/MessagingTests.cs (84%) rename tests/{ => L1L2RedisCache}/System/PerformanceTests.cs (79%) rename tests/{ => L1L2RedisCache}/System/ReliabilityTests.cs (97%) rename tests/{ => L1L2RedisCache}/System/appsettings.json (57%) create mode 100644 tests/MessagingRedisCache/System/DefaultMessagingTests.cs create mode 100644 tests/MessagingRedisCache/System/HybridCacheTests.cs create mode 100644 tests/MessagingRedisCache/System/KeyeventMessagingTests.cs create mode 100644 tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs create mode 100644 tests/MessagingRedisCache/System/MessagingRedisCache.Tests.System.csproj create mode 100644 tests/MessagingRedisCache/System/MessagingTestsBase.cs create mode 100644 tests/MessagingRedisCache/System/TestsBase.cs create mode 100644 tests/MessagingRedisCache/System/appsettings.json create mode 100644 tests/MessagingRedisCache/Unit/MessagingRedisCache.Tests.Unit.csproj create mode 100644 tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs delete mode 100644 tests/System/L1L2RedisCache.Tests.System.csproj delete mode 100644 tests/Unit/L1L2RedisCache.Tests.Unit.csproj delete mode 100644 tests/Unit/L1L2RedisCacheTests.cs diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c15b68e..900fc41 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,7 +25,7 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: 9 - - name: Build - run: dotnet build -c Release + - name: Pack + run: dotnet pack -c Release - name: Publish run: dotnet nuget push "**/*.nupkg" -k ${{ secrets.NUGET_KEY }} -n -s https://api.nuget.org/v3/index.json --skip-duplicate \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..55292cc --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + "version": "0.2.0", + "configurations": + [ + { + "name": "MessagingRedisCache Lab", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceFolder}/labs/MessagingRedisCache/bin/Debug/net9.0/MessagingRedisCache.Lab.dll", + "args": [], + "cwd": "${workspaceFolder}/labs/MessagingRedisCache", + "stopAtEntry": false, + "env": + { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + ] +} diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..8da37cb --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,12 @@ + + + latest-all + true + true + enable + latest + enable + git + https://github.com/null-d3v/L1L2RedisCache.git + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..42660ac --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,24 @@ + + + true + true + + + + + + + + + + + + + + + + + + + + diff --git a/L1L2RedisCache.sln b/L1L2RedisCache.sln index 7c884b3..e18e42f 100644 --- a/L1L2RedisCache.sln +++ b/L1L2RedisCache.sln @@ -3,13 +3,25 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache", "src\L1L2RedisCache.csproj", "{71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FEEBBE29-BAE7-4407-95D5-EB1F56F6BDA3}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{79FE29CD-A4E5-46BB-9FC8-5EC921CFE5F3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache", "src\L1L2RedisCache\L1L2RedisCache.csproj", "{9C09D590-3186-4E27-8103-66C2D6CAFDF2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.Unit", "tests\Unit\L1L2RedisCache.Tests.Unit.csproj", "{8791FCF7-078D-44A5-AC59-C7C2CE469D3F}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagingRedisCache", "src\MessagingRedisCache\MessagingRedisCache.csproj", "{71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.System", "tests\System\L1L2RedisCache.Tests.System.csproj", "{6A825E82-5BF4-43A0-BA08-9CB000FB232A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E6F22576-963C-4CE3-A653-FBC7DEA5F10A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.System", "tests\MessagingRedisCache\System\MessagingRedisCache.Tests.System.csproj", "{DF934775-FF05-462D-8514-497F68BB8A45}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.Unit", "tests\MessagingRedisCache\Unit\MessagingRedisCache.Tests.Unit.csproj", "{8791FCF7-078D-44A5-AC59-C7C2CE469D3F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "L1L2RedisCache", "L1L2RedisCache", "{E4234EE7-DAAE-40F5-A24F-14C9C2B5F9BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.System", "tests\L1L2RedisCache\System\L1L2RedisCache.Tests.System.csproj", "{2BA929A4-78E0-4C29-A676-1F5339C99245}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "labs", "labs", "{3E40FF50-EE26-49F6-81B8-94DE4CC9BDF2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagingRedisCache.Lab", "labs\MessagingRedisCache\MessagingRedisCache.Lab.csproj", "{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -48,20 +60,60 @@ Global {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|x64.Build.0 = Release|Any CPU {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|x86.ActiveCfg = Release|Any CPU {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|x86.Build.0 = Release|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|x64.ActiveCfg = Debug|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|x64.Build.0 = Debug|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|x86.ActiveCfg = Debug|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Debug|x86.Build.0 = Debug|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|Any CPU.Build.0 = Release|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|x64.ActiveCfg = Release|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|x64.Build.0 = Release|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|x86.ActiveCfg = Release|Any CPU - {6A825E82-5BF4-43A0-BA08-9CB000FB232A}.Release|x86.Build.0 = Release|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x64.ActiveCfg = Debug|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x64.Build.0 = Debug|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x86.ActiveCfg = Debug|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x86.Build.0 = Debug|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Release|Any CPU.Build.0 = Release|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Release|x64.ActiveCfg = Release|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Release|x64.Build.0 = Release|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Release|x86.ActiveCfg = Release|Any CPU + {DF934775-FF05-462D-8514-497F68BB8A45}.Release|x86.Build.0 = Release|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x64.ActiveCfg = Debug|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x64.Build.0 = Debug|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x86.ActiveCfg = Debug|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x86.Build.0 = Debug|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|Any CPU.Build.0 = Release|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x64.ActiveCfg = Release|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x64.Build.0 = Release|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x86.ActiveCfg = Release|Any CPU + {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x86.Build.0 = Release|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x64.ActiveCfg = Debug|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x64.Build.0 = Debug|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x86.ActiveCfg = Debug|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x86.Build.0 = Debug|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|Any CPU.Build.0 = Release|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x64.ActiveCfg = Release|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x64.Build.0 = Release|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x86.ActiveCfg = Release|Any CPU + {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x86.Build.0 = Release|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x64.ActiveCfg = Debug|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x64.Build.0 = Debug|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x86.ActiveCfg = Debug|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x86.Build.0 = Debug|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|Any CPU.Build.0 = Release|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x64.ActiveCfg = Release|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x64.Build.0 = Release|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x86.ActiveCfg = Release|Any CPU + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution - {6A825E82-5BF4-43A0-BA08-9CB000FB232A} = {79FE29CD-A4E5-46BB-9FC8-5EC921CFE5F3} + {DF934775-FF05-462D-8514-497F68BB8A45} = {E6F22576-963C-4CE3-A653-FBC7DEA5F10A} + {9C09D590-3186-4E27-8103-66C2D6CAFDF2} = {FEEBBE29-BAE7-4407-95D5-EB1F56F6BDA3} + {E4234EE7-DAAE-40F5-A24F-14C9C2B5F9BD} = {E6F22576-963C-4CE3-A653-FBC7DEA5F10A} + {2BA929A4-78E0-4C29-A676-1F5339C99245} = {E4234EE7-DAAE-40F5-A24F-14C9C2B5F9BD} + {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A} = {3E40FF50-EE26-49F6-81B8-94DE4CC9BDF2} EndGlobalSection EndGlobal diff --git a/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj b/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj new file mode 100644 index 0000000..87502fd --- /dev/null +++ b/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj @@ -0,0 +1,19 @@ + + + EXTEXP0018; + Exe + net9.0 + + + + + + + + + + + + + + diff --git a/labs/MessagingRedisCache/Program.cs b/labs/MessagingRedisCache/Program.cs new file mode 100644 index 0000000..3f87ffd --- /dev/null +++ b/labs/MessagingRedisCache/Program.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +var services = new ServiceCollection(); +services.AddLogging(builder => builder.AddConsole().AddFilter(null, LogLevel.Trace)); +services.AddHybridCache(); +services.AddMessagingRedisCache(options => +{ + options.Configuration = "redis"; + options.InstanceName = "MessagingRedisCache:Test:"; +}); +var serviceProvider = services.BuildServiceProvider(); + +var hybridCache = serviceProvider + .GetRequiredService(); + +var key = Guid.NewGuid().ToString(); +var value = Guid.NewGuid().ToString(); + +await hybridCache + .SetAsync( + key, + value, + new HybridCacheEntryOptions + { + Expiration = TimeSpan.FromHours(1), + }) + .ConfigureAwait(false); + +var testValue = await hybridCache + .GetOrCreateAsync( + key, + cancellationToken => + ValueTask.FromResult(null)) + .ConfigureAwait(false); + +var test = ""; diff --git a/src/Configuration/ServiceCollectionExtensions.cs b/src/Configuration/ServiceCollectionExtensions.cs deleted file mode 100644 index a3989b1..0000000 --- a/src/Configuration/ServiceCollectionExtensions.cs +++ /dev/null @@ -1,104 +0,0 @@ -using L1L2RedisCache; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.StackExchangeRedis; -using Microsoft.Extensions.Options; -using StackExchange.Redis; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Extension methods for setting up L1L2RedisCache related services in an Microsoft.Extensions.DependencyInjection.IServiceCollection. -/// -public static class ServiceCollectionExtensions -{ - /// - /// Adds L1L2RedisCache distributed caching services to the specified IServiceCollection. - /// - /// The IServiceCollection so that additional calls can be chained. - public static IServiceCollection AddL1L2RedisCache( - this IServiceCollection services, - Action setupAction) - { - if (setupAction == null) - { - throw new ArgumentNullException( - nameof(setupAction)); - } - - var l1L2RedisCacheOptions = new L1L2RedisCacheOptions(); - setupAction.Invoke(l1L2RedisCacheOptions); - - services.AddOptions(); - services.Configure(setupAction); - services.Configure( - (options) => - { - if (options.ConnectionMultiplexerFactory == null) - { - if (options.ConfigurationOptions != null) - { - options.ConnectionMultiplexerFactory = () => - Task.FromResult( - ConnectionMultiplexer.Connect( - options.ConfigurationOptions) as IConnectionMultiplexer); - } - else if (!string.IsNullOrEmpty(options.Configuration)) - { - options.ConnectionMultiplexerFactory = () => - Task.FromResult( - ConnectionMultiplexer.Connect( - options.Configuration) as IConnectionMultiplexer); - } - } - }); - services.AddMemoryCache(); - services.AddSingleton( - serviceProvider => new Func( - () => new RedisCache( - serviceProvider.GetRequiredService>()))); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton( - serviceProvider => - { - var options = serviceProvider - .GetRequiredService>() - .Value; - - return options.MessagingType switch - { - MessagingType.Default => - serviceProvider.GetRequiredService(), - _ => - serviceProvider.GetRequiredService(), - }; - }); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton( - serviceProvider => - { - var options = serviceProvider - .GetRequiredService>() - .Value; - - return options.MessagingType switch - { - MessagingType.Default => - serviceProvider.GetRequiredService(), - MessagingType.KeyeventNotifications => - serviceProvider.GetRequiredService(), - MessagingType.KeyspaceNotifications => - serviceProvider.GetRequiredService(), - _ => throw new NotImplementedException(), - }; - }); - - return services; - } -} diff --git a/src/L1L2RedisCache.csproj b/src/L1L2RedisCache.csproj deleted file mode 100644 index 637ecfc..0000000 --- a/src/L1L2RedisCache.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - latest-recommended - true - true - true - enable - latest - CA1724;CA1812;SYSLIB1006; - enable - README.md - git - https://github.com/null-d3v/L1L2RedisCache.git - netstandard2.1 - - - - - - - - - - - - - - - diff --git a/src/L1L2RedisCache/Configuration/ServiceCollectionExtensions.cs b/src/L1L2RedisCache/Configuration/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..751d3aa --- /dev/null +++ b/src/L1L2RedisCache/Configuration/ServiceCollectionExtensions.cs @@ -0,0 +1,29 @@ +using MessagingRedisCache; +using Microsoft.Extensions.Caching.Distributed; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for setting up L1L2RedisCache related services in an Microsoft.Extensions.DependencyInjection.IServiceCollection. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds L1L2RedisCache distributed caching services to the specified IServiceCollection. + /// + /// The IServiceCollection so that additional calls can be chained. + public static IServiceCollection AddL1L2RedisCache( + this IServiceCollection services, + Action setupAction) + { + ArgumentNullException.ThrowIfNull(setupAction); + + services + .AddMessagingRedisCache(setupAction) + .AddMemoryCacheSubscriber(); + services.AddSingleton(); + services.AddMemoryCache(); + + return services; + } +} \ No newline at end of file diff --git a/src/HashEntryArrayExtensions.cs b/src/L1L2RedisCache/HashEntryArrayExtensions.cs similarity index 96% rename from src/HashEntryArrayExtensions.cs rename to src/L1L2RedisCache/HashEntryArrayExtensions.cs index d3ad2ff..15a842d 100644 --- a/src/HashEntryArrayExtensions.cs +++ b/src/L1L2RedisCache/HashEntryArrayExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Distributed; using StackExchange.Redis; namespace L1L2RedisCache; diff --git a/src/L1L2RedisCache.cs b/src/L1L2RedisCache/L1L2RedisCache.cs similarity index 55% rename from src/L1L2RedisCache.cs rename to src/L1L2RedisCache/L1L2RedisCache.cs index 22bc1d2..38fbd34 100644 --- a/src/L1L2RedisCache.cs +++ b/src/L1L2RedisCache/L1L2RedisCache.cs @@ -1,138 +1,58 @@ -using Microsoft.Extensions.Caching.Distributed; +using MessagingRedisCache; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StackExchange.Redis; +using System.Diagnostics.CodeAnalysis; namespace L1L2RedisCache; /// /// A distributed cache implementation using both memory and Redis. /// -public sealed class L1L2RedisCache : - IDisposable, +public class L1L2RedisCache( + IMemoryCache l1Cache, + IMessagePublisher messagePublisher, + IMessageSubscriber messageSubscriber, + IMessagingConfigurationVerifier messagingConfigurationVerifier, + IOptions messagingRedisCacheOptionsAccessor, + ILogger? logger = null) : + MessagingRedisCache.MessagingRedisCache( + messagePublisher, + messageSubscriber, + messagingConfigurationVerifier, + messagingRedisCacheOptionsAccessor, + logger), IDistributedCache { /// - /// Initializes a new instance of L1L2RedisCache. + /// The IMemoryCache for L1. /// - public L1L2RedisCache( - IMemoryCache l1Cache, - IOptions l1l2RedisCacheOptionsAccessor, - Func l2CacheAccessor, - IMessagePublisher messagePublisher, - IMessageSubscriber messageSubscriber, - IMessagingConfigurationVerifier messagingConfigurationVerifier, - ILogger? logger = null) - { - L1Cache = l1Cache ?? + public IMemoryCache L1Cache { get; } = + l1Cache ?? throw new ArgumentNullException( nameof(l1Cache)); - L1L2RedisCacheOptions = l1l2RedisCacheOptionsAccessor?.Value ?? - throw new ArgumentNullException( - nameof(l1l2RedisCacheOptionsAccessor)); - L2Cache = l2CacheAccessor?.Invoke() ?? - throw new ArgumentNullException( - nameof(l2CacheAccessor)); - Logger = logger ?? - NullLogger.Instance; - MessagePublisher = messagePublisher ?? - throw new ArgumentNullException( - nameof(messagePublisher)); - MessageSubscriber = messageSubscriber ?? - throw new ArgumentNullException( - nameof(messageSubscriber)); - MessagingConfigurationVerifier = messagingConfigurationVerifier ?? - throw new ArgumentNullException( - nameof(l1l2RedisCacheOptionsAccessor)); - - Database = new Lazy(() => - L1L2RedisCacheOptions - .ConnectionMultiplexerFactory!() - .GetAwaiter() - .GetResult() - .GetDatabase( - L1L2RedisCacheOptions - .ConfigurationOptions? - .DefaultDatabase ?? -1)); - - SubscribeCancellationTokenSource = new CancellationTokenSource(); - _ = SubscribeAsync( - SubscribeCancellationTokenSource.Token); - } - private static SemaphoreSlim KeySemaphore { get; } = + private SemaphoreSlim KeySemaphore { get; } = new SemaphoreSlim(1, 1); - /// - /// The StackExchange.Redis.IDatabase for the . - /// - public Lazy Database { get; } - - /// - /// The IMemoryCache for L1. - /// - public IMemoryCache L1Cache { get; } - - /// - /// Configuration options. - /// - public L1L2RedisCacheOptions L1L2RedisCacheOptions { get; } - - /// - /// The IDistributedCache for L2. - /// - public IDistributedCache L2Cache { get; } - - /// - /// Optional. The logger. - /// - public ILogger Logger { get; } - - /// - /// The pub/sub publisher. - /// - public IMessagePublisher MessagePublisher { get; } - - /// - /// The pub/sub subscriber. - /// - public IMessageSubscriber MessageSubscriber { get; } - - /// - /// The messaging configuration verifier. - /// - public IMessagingConfigurationVerifier MessagingConfigurationVerifier { get; } - - private bool IsDisposed { get; set; } - private CancellationTokenSource SubscribeCancellationTokenSource { get; set; } - - /// - /// Releases all resources used by the current instance. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - /// /// Gets a value with the given key. /// /// A string identifying the requested value. /// The located value or null. - public byte[]? Get(string key) + [SuppressMessage("Reliability", "CA2000")] + public new byte[]? Get(string key) { - var value = L1Cache.Get( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}") as byte[]; + var value = L1Cache.Get(key) as byte[]; if (value == null) { if (Database.Value.KeyExists( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}")) + $"{MessagingRedisCacheOptions.InstanceName}{key}")) { - var semaphore = GetOrCreateLock( + var semaphore = GetOrCreateKeyLock( key, null); semaphore.Wait(); @@ -147,7 +67,7 @@ public void Dispose() key, value!, distributedCacheEntryOptions); - SetLock( + SetKeyLock( key, semaphore, distributedCacheEntryOptions); @@ -168,21 +88,20 @@ public void Dispose() /// A string identifying the requested value. /// Optional. The System.Threading.CancellationToken used to propagate notifications that the operation should be canceled. /// The System.Threading.Tasks.Task that represents the asynchronous operation, containing the located value or null. - public async Task GetAsync( + public new async Task GetAsync( string key, CancellationToken token = default) { - var value = L1Cache.Get( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}") as byte[]; + var value = L1Cache.Get(key) as byte[]; if (value == null) { if (await Database.Value .KeyExistsAsync( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}") + $"{MessagingRedisCacheOptions.InstanceName}{key}") .ConfigureAwait(false)) { - var semaphore = await GetOrCreateLockAsync( + var semaphore = await GetOrCreateKeyLockAsync( key, null, token) @@ -202,7 +121,7 @@ await semaphore key, value!, distributedCacheEntryOptions); - SetLock( + SetKeyLock( key, semaphore, distributedCacheEntryOptions); @@ -217,48 +136,21 @@ await semaphore return value; } - /// - /// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any). - /// - /// A string identifying the requested value. - public void Refresh(string key) - { - L2Cache.Refresh(key); - } - - /// - /// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any). - /// - /// A string identifying the requested value. - /// Optional. The System.Threading.CancellationToken used to propagate notifications that the operation should be canceled. - /// The System.Threading.Tasks.Task that represents the asynchronous operation. - public async Task RefreshAsync( - string key, - CancellationToken token = default) - { - await L2Cache - .RefreshAsync(key, token) - .ConfigureAwait(false); - } - /// /// Removes the value with the given key. /// /// A string identifying the requested value. - public void Remove(string key) + [SuppressMessage("Reliability", "CA2000")] + public new void Remove(string key) { - var semaphore = GetOrCreateLock(key, null); + var semaphore = GetOrCreateKeyLock(key, null); semaphore.Wait(); try { - L2Cache.Remove(key); + base.Remove(key); + L1Cache.Remove(key); L1Cache.Remove( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}"); - MessagePublisher.Publish( - Database.Value.Multiplexer, - key); - L1Cache.Remove( - $"{L1L2RedisCacheOptions.LockKeyPrefix}{key}"); + $"{MessagingRedisCacheOptions.Id}{key}"); } finally { @@ -272,11 +164,11 @@ public void Remove(string key) /// A string identifying the requested value. /// Optional. The System.Threading.CancellationToken used to propagate notifications that the operation should be canceled. /// The System.Threading.Tasks.Task that represents the asynchronous operation. - public async Task RemoveAsync( + public new async Task RemoveAsync( string key, CancellationToken token = default) { - var semaphore = await GetOrCreateLockAsync( + var semaphore = await GetOrCreateKeyLockAsync( key, null, token) @@ -286,19 +178,12 @@ await semaphore .ConfigureAwait(false); try { - await L2Cache + await base .RemoveAsync(key, token) .ConfigureAwait(false); + L1Cache.Remove(key); L1Cache.Remove( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}"); - await MessagePublisher - .PublishAsync( - Database.Value.Multiplexer, - key, - token) - .ConfigureAwait(false); - L1Cache.Remove( - $"{L1L2RedisCacheOptions.LockKeyPrefix}{key}"); + $"{MessagingRedisCacheOptions.Id}{key}"); } finally { @@ -312,23 +197,21 @@ await MessagePublisher /// A string identifying the requested value. /// The value to set in the cache. /// The cache options for the value. - public void Set( + [SuppressMessage("Reliability", "CA2000")] + public new void Set( string key, byte[] value, DistributedCacheEntryOptions options) { - var semaphore = GetOrCreateLock( + var semaphore = GetOrCreateKeyLock( key, options); semaphore.Wait(); try { - L2Cache.Set( + base.Set( key, value, options); SetMemoryCache( key, value, options); - MessagePublisher.Publish( - Database.Value.Multiplexer, - key); } finally { @@ -344,13 +227,13 @@ public void Set( /// The cache options for the value. /// Optional. The System.Threading.CancellationToken used to propagate notifications that the operation should be canceled. /// The System.Threading.Tasks.Task that represents the asynchronous operation. - public async Task SetAsync( + public new async Task SetAsync( string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) { - var semaphore = await GetOrCreateLockAsync( + var semaphore = await GetOrCreateKeyLockAsync( key, options, token) @@ -360,7 +243,7 @@ await semaphore .ConfigureAwait(false); try { - await L2Cache + await base .SetAsync( key, value, @@ -369,12 +252,6 @@ await L2Cache .ConfigureAwait(false); SetMemoryCache( key, value, options); - await MessagePublisher - .PublishAsync( - Database.Value.Multiplexer, - key, - token) - .ConfigureAwait(false); } finally { @@ -382,21 +259,6 @@ await MessagePublisher } } - private void Dispose(bool isDisposing) - { - if (IsDisposed) - { - return; - } - - if (isDisposing) - { - SubscribeCancellationTokenSource.Dispose(); - } - - IsDisposed = true; - } - private HashEntry[] GetHashEntries(string key) { var hashEntries = Array.Empty(); @@ -404,7 +266,7 @@ private HashEntry[] GetHashEntries(string key) try { hashEntries = Database.Value.HashGetAll( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}"); + $"{MessagingRedisCacheOptions.InstanceName}{key}"); } catch (RedisServerException) { } @@ -419,7 +281,7 @@ private async Task GetHashEntriesAsync(string key) { hashEntries = await Database.Value .HashGetAllAsync( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}") + $"{MessagingRedisCacheOptions.InstanceName}{key}") .ConfigureAwait(false); } catch (RedisServerException) { } @@ -427,7 +289,7 @@ private async Task GetHashEntriesAsync(string key) return hashEntries; } - private SemaphoreSlim GetOrCreateLock( + private SemaphoreSlim GetOrCreateKeyLock( string key, DistributedCacheEntryOptions? distributedCacheEntryOptions) { @@ -435,7 +297,7 @@ private SemaphoreSlim GetOrCreateLock( try { return L1Cache.GetOrCreate( - $"{L1L2RedisCacheOptions.LockKeyPrefix}{key}", + $"{MessagingRedisCacheOptions.Id}{key}", cacheEntry => { cacheEntry.AbsoluteExpiration = @@ -454,7 +316,8 @@ private SemaphoreSlim GetOrCreateLock( } } - private async Task GetOrCreateLockAsync( + [SuppressMessage("Reliability", "CA2000")] + private async Task GetOrCreateKeyLockAsync( string key, DistributedCacheEntryOptions? distributedCacheEntryOptions, CancellationToken cancellationToken = default) @@ -466,7 +329,7 @@ await KeySemaphore { return await L1Cache .GetOrCreateAsync( - $"{L1L2RedisCacheOptions.LockKeyPrefix}{key}", + $"{MessagingRedisCacheOptions.Id}{key}", cacheEntry => { cacheEntry.AbsoluteExpiration = @@ -486,7 +349,7 @@ await KeySemaphore } } - private SemaphoreSlim SetLock( + private SemaphoreSlim SetKeyLock( string key, SemaphoreSlim semaphore, DistributedCacheEntryOptions distributedCacheEntryOptions) @@ -502,7 +365,7 @@ private SemaphoreSlim SetLock( }; return L1Cache.Set( - $"{L1L2RedisCacheOptions.LockKeyPrefix}{key}", + $"{MessagingRedisCacheOptions.Id}{key}", semaphore, memoryCacheEntryOptions); } @@ -525,50 +388,9 @@ private void SetMemoryCache( if (!memoryCacheEntryOptions.SlidingExpiration.HasValue) { L1Cache.Set( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}", + key, value, memoryCacheEntryOptions); } } - - private async Task SubscribeAsync( - CancellationToken cancellationToken = default) - { - while (true) - { - try - { - if (!await MessagingConfigurationVerifier - .VerifyConfigurationAsync( - Database.Value, - cancellationToken) - .ConfigureAwait(false)) - { - Logger.MessagingConfigurationInvalid( - L1L2RedisCacheOptions.MessagingType); - } - - await MessageSubscriber - .SubscribeAsync( - Database.Value.Multiplexer, - cancellationToken) - .ConfigureAwait(false); - break; - } - catch (RedisConnectionException redisConnectionException) - { - Logger.SubscriberFailed( - L1L2RedisCacheOptions - .SubscriberRetryDelay, - redisConnectionException); - - await Task - .Delay( - L1L2RedisCacheOptions - .SubscriberRetryDelay, - cancellationToken) - .ConfigureAwait(false); - } - } - } -} +} \ No newline at end of file diff --git a/src/L1L2RedisCache/L1L2RedisCache.csproj b/src/L1L2RedisCache/L1L2RedisCache.csproj new file mode 100644 index 0000000..38b0c04 --- /dev/null +++ b/src/L1L2RedisCache/L1L2RedisCache.csproj @@ -0,0 +1,24 @@ + + + true + CA1724; + README.md + true + net8.0;net9.0 + + + + + + + + + + + + + + + + + diff --git a/src/L1L2RedisCache/README.md b/src/L1L2RedisCache/README.md new file mode 100644 index 0000000..cdc8e77 --- /dev/null +++ b/src/L1L2RedisCache/README.md @@ -0,0 +1,41 @@ +# L1L2RedisCache + +`L1L2RedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) with emphasis on performance. It leverages [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a level 2 cache, with level 1 evictions being managed via [Redis pub/sub](https://redis.io/topics/pubsub) by way of [`MemoryCache`](lol.com). + +`L1L2RedisCache` is heavily inspired by development insights provided over the past several years by [StackOverflow](https://stackoverflow.com/). It attempts to simplify those concepts into a highly accessible `IDistributedCache` implementation that is more performant. + +I expect to gracefully decomission this project when [`StackExchange.Redis`](https://github.com/StackExchange/StackExchange.Redis) has [client-side caching](https://redis.io/docs/latest/develop/use/client-side-caching/) support. + +## Configuration + +It is intended that L1L12RedisCache be used as an `IDistributedCache` implementation. + +`L1L2RedisCache` can be registered during startup with the following `IServiceCollection` extension method: + +``` +services.AddL1L2RedisCache(options => +{ + options.Configuration = "localhost"; + options.InstanceName = "Namespace:Prefix:"; +}); +``` + +`L1L2RedisCache` uses `MessagingRedisCacheOptions` which extend the standard `RedisCache` [`RedisCacheOptions`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCacheOptions.cs). The following additional customizations are supported: + +### MessagingType + +The type of messaging system to use for L1 memory cache eviction. + +| MessagingType | Description | Suggestion | +| - | - | - | +| `Default` | Use standard `L1L2RedisCache` [pub/sub](https://redis.io/topics/pubsub) messages for L1 memory cache eviction. | Default behavior. The Redis server requires no additional configuration. | +| `KeyeventNotifications` | Use [keyevent notifications](https://redis.io/topics/notifications) for L1 memory eviction instead of standard `L1L2RedisCache` [pub/sub](https://redis.io/topics/pubsub) messages. The Redis server must have keyevent notifications enabled. | This is only advisable if the Redis server is already using [keyevent notifications](https://redis.io/topics/notifications) with at least a `ghE` configuration and the majority of keys in the server are managed by `L1L2RedisCache`. | +| `KeyspaceNotifications` | Use [keyspace notifications](https://redis.io/topics/notifications) for L1 memory eviction instead of standard `L1L2RedisCache` [pub/sub](https://redis.io/topics/pubsub) messages. The Redis server must have keyspace notifications enabled. | This is only advisable if the Redis server is already using [keyevent notifications](https://redis.io/topics/notifications) with at least a `ghK` configuration and the majority of keys in the server are managed by `L1L2RedisCache`. | + +## Performance + +L1L2RedisCache will generally outperform `RedisCache`, especially in cases of high volume or large cache entries. As entries are opportunistically pulled from memory instead of Redis, costs of latency, network, and Redis operations are avoided. Respective performance gains will rely heavily on the impact of afforementioned factors. + +## Considerations + +Due to the complex nature of a distributed L1 memory cache, cache entries with sliding expirations are only stored in L2 (Redis). These entries will show no performance improvement over the standard `RedisCache`, but incur no performance penalty. diff --git a/src/L1L2RedisCacheOptions.cs b/src/L1L2RedisCacheOptions.cs deleted file mode 100644 index 230b178..0000000 --- a/src/L1L2RedisCacheOptions.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.Extensions.Caching.StackExchangeRedis; -using Microsoft.Extensions.Options; - -namespace L1L2RedisCache; - -/// -/// Configuration options for L1L2RedisCache. -/// -public sealed class L1L2RedisCacheOptions : - RedisCacheOptions, IOptions -{ - /// - /// Initializes a new instance of L1L2RedisCacheOptions. - /// - public L1L2RedisCacheOptions() : base() { } - - /// - /// Unique identifier for the operating instance. - /// - public Guid Id { get; } = Guid.NewGuid(); - - /// - /// The pub/sub channel name. - /// - public string Channel => $"{KeyPrefix}Channel"; - - /// - /// A prefix to be applied to all cache keys. - /// - public string KeyPrefix => InstanceName ?? string.Empty; - - /// - /// A prefix to be applied to all L1 lock cache keys. - /// - public string LockKeyPrefix => $"{KeyPrefix}{Id}"; - - /// - /// The type of messaging to use for L1 memory cache eviction. - /// - public MessagingType MessagingType { get; set; } = - MessagingType.Default; - - /// - /// The duration of time to delay before retrying subscriber intialization. - /// - public TimeSpan SubscriberRetryDelay { get; set; } = - TimeSpan.FromSeconds(5); - - L1L2RedisCacheOptions IOptions.Value => this; -} diff --git a/src/Messaging/DefaultMessagePublisher.cs b/src/Messaging/DefaultMessagePublisher.cs deleted file mode 100644 index f0c81c0..0000000 --- a/src/Messaging/DefaultMessagePublisher.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.Extensions.Options; -using StackExchange.Redis; -using System.Text.Json; - -namespace L1L2RedisCache; - -internal sealed class DefaultMessagePublisher( - IOptions jsonSerializerOptionsAccessor, - IOptions l1L2RedisCacheOptionsOptionsAccessor) : - IMessagePublisher -{ - public JsonSerializerOptions JsonSerializerOptions { get; set; } = - jsonSerializerOptionsAccessor.Value; - public L1L2RedisCacheOptions L1L2RedisCacheOptions { get; set; } = - l1L2RedisCacheOptionsOptionsAccessor.Value; - - public void Publish( - IConnectionMultiplexer connectionMultiplexer, - string key) - { - connectionMultiplexer - .GetSubscriber() - .Publish( - new RedisChannel( - L1L2RedisCacheOptions.Channel, - RedisChannel.PatternMode.Literal), - JsonSerializer.Serialize( - new CacheMessage - { - Key = key, - PublisherId = L1L2RedisCacheOptions.Id, - }, - JsonSerializerOptions)); - } - - public async Task PublishAsync( - IConnectionMultiplexer connectionMultiplexer, - string key, - CancellationToken cancellationToken = default) - { - await connectionMultiplexer - .GetSubscriber() - .PublishAsync( - new RedisChannel( - L1L2RedisCacheOptions.Channel, - RedisChannel.PatternMode.Literal), - JsonSerializer.Serialize( - new CacheMessage - { - Key = key, - PublisherId = L1L2RedisCacheOptions.Id, - }, - JsonSerializerOptions)) - .ConfigureAwait(false); - } -} diff --git a/src/CacheMessage.cs b/src/MessagingRedisCache/CacheMessage.cs similarity index 92% rename from src/CacheMessage.cs rename to src/MessagingRedisCache/CacheMessage.cs index 2b80666..0c4a5c2 100644 --- a/src/CacheMessage.cs +++ b/src/MessagingRedisCache/CacheMessage.cs @@ -1,4 +1,4 @@ -namespace L1L2RedisCache; +namespace MessagingRedisCache; /// /// A Redis pub/sub message indicating a cache value has changed. diff --git a/src/Configuration/IMessagingConfigurationVerifier.cs b/src/MessagingRedisCache/Configuration/IMessagingConfigurationVerifier.cs similarity index 95% rename from src/Configuration/IMessagingConfigurationVerifier.cs rename to src/MessagingRedisCache/Configuration/IMessagingConfigurationVerifier.cs index 53dadab..3f15506 100644 --- a/src/Configuration/IMessagingConfigurationVerifier.cs +++ b/src/MessagingRedisCache/Configuration/IMessagingConfigurationVerifier.cs @@ -1,6 +1,6 @@ using StackExchange.Redis; -namespace L1L2RedisCache; +namespace MessagingRedisCache; /// /// Verifies Redis configuration settings. diff --git a/src/MessagingRedisCache/Configuration/IMessagingRedisCacheBuilder.cs b/src/MessagingRedisCache/Configuration/IMessagingRedisCacheBuilder.cs new file mode 100644 index 0000000..cdf810b --- /dev/null +++ b/src/MessagingRedisCache/Configuration/IMessagingRedisCacheBuilder.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace MessagingRedisCache; + +/// +/// A builder abstraction for configuring a messaging Redis cache. +/// +public interface IMessagingRedisCacheBuilder +{ + IServiceCollection Services { get; } +} \ No newline at end of file diff --git a/src/Configuration/MessagingConfigurationVerifier.cs b/src/MessagingRedisCache/Configuration/MessagingConfigurationVerifier.cs similarity index 71% rename from src/Configuration/MessagingConfigurationVerifier.cs rename to src/MessagingRedisCache/Configuration/MessagingConfigurationVerifier.cs index 4e2cf1e..1291c48 100644 --- a/src/Configuration/MessagingConfigurationVerifier.cs +++ b/src/MessagingRedisCache/Configuration/MessagingConfigurationVerifier.cs @@ -1,10 +1,10 @@ using Microsoft.Extensions.Options; using StackExchange.Redis; -namespace L1L2RedisCache; +namespace MessagingRedisCache; -internal sealed class MessagingConfigurationVerifier( - IOptions l1L2RedisCacheOptionsOptionsAccessor) : +public sealed class MessagingConfigurationVerifier( + IOptions messagingRedisCacheOptionsAccessor) : IMessagingConfigurationVerifier { private const string config = "notify-keyspace-events"; @@ -19,18 +19,20 @@ static MessagingConfigurationVerifier() }; } - internal static IDictionary NotifyKeyspaceEventsConfig { get; } + public static IDictionary NotifyKeyspaceEventsConfig { get; } - public L1L2RedisCacheOptions L1L2RedisCacheOptions { get; } = - l1L2RedisCacheOptionsOptionsAccessor.Value; + public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; } = + messagingRedisCacheOptionsAccessor.Value; public async Task VerifyConfigurationAsync( IDatabase database, - CancellationToken _ = default) + CancellationToken cancellationToken = default) { + ArgumentNullException.ThrowIfNull(database); + var isVerified = NotifyKeyspaceEventsConfig .TryGetValue( - L1L2RedisCacheOptions.MessagingType, + MessagingRedisCacheOptions.MessagingType, out var expectedValues); var configValue = (await database diff --git a/src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilder.cs b/src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilder.cs new file mode 100644 index 0000000..2a37955 --- /dev/null +++ b/src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilder.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace MessagingRedisCache; + +internal class MessagingRedisCacheBuilder( + IServiceCollection services) : + IMessagingRedisCacheBuilder +{ + public IServiceCollection Services { get; } = + services; +} \ No newline at end of file diff --git a/src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilderExtensions.cs b/src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilderExtensions.cs new file mode 100644 index 0000000..7ea5ddc --- /dev/null +++ b/src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilderExtensions.cs @@ -0,0 +1,41 @@ +using MessagingRedisCache; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for setting up messaging Redis cache related services. +/// +public static class MessagingRedisCacheBuilderExtensions +{ + public static IMessagingRedisCacheBuilder AddMemoryCacheSubscriber( + this IMessagingRedisCacheBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.AddSingleton( + serviceProvider => + { + var options = serviceProvider + .GetRequiredService>() + .Value; + + return options.MessagingType switch + { + MessagingType.Default => + serviceProvider.GetRequiredService(), + MessagingType.KeyeventNotifications => + serviceProvider.GetRequiredService(), + MessagingType.KeyspaceNotifications => + serviceProvider.GetRequiredService(), + _ => throw new NotImplementedException(), + }; + }); + + return builder; + } +} \ No newline at end of file diff --git a/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs b/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..f07728e --- /dev/null +++ b/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs @@ -0,0 +1,76 @@ +using MessagingRedisCache; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for setting up messaging Redis cache related services in an Microsoft.Extensions.DependencyInjection.IServiceCollection. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds messaging Redis caching services to the specified IServiceCollection. + /// + /// A IMessagingRedisCacheBuilder so that additional calls can be chained. + [SuppressMessage("Performance", "CA1849")] + public static IMessagingRedisCacheBuilder AddMessagingRedisCache( + this IServiceCollection services, + Action setupAction) + { + ArgumentNullException.ThrowIfNull(setupAction); + + services.AddOptions(); + services.Configure(setupAction); + services.Configure( + options => + { + if (options.ConnectionMultiplexerFactory == null) + { + if (options.ConfigurationOptions != null) + { + options.ConnectionMultiplexerFactory = () => + Task.FromResult( + ConnectionMultiplexer.Connect( + options.ConfigurationOptions) as IConnectionMultiplexer); + } + else if (!string.IsNullOrEmpty(options.Configuration)) + { + options.ConnectionMultiplexerFactory = () => + Task.FromResult( + ConnectionMultiplexer.Connect( + options.Configuration) as IConnectionMultiplexer); + } + } + }); + services.TryAddSingleton(); + services.TryAddSingleton(); + + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton( + serviceProvider => + { + var options = serviceProvider + .GetRequiredService>() + .Value; + + return options.MessagingType switch + { + MessagingType.Default => + serviceProvider.GetRequiredService(), + MessagingType.KeyeventNotifications or MessagingType.KeyspaceNotifications => + serviceProvider.GetRequiredService(), + _ => + throw new NotImplementedException(), + }; + }); + + services.TryAddSingleton(); + + return new MessagingRedisCacheBuilder(services); + } +} diff --git a/src/MessagingRedisCache/Messaging/DefaultMessagePublisher.cs b/src/MessagingRedisCache/Messaging/DefaultMessagePublisher.cs new file mode 100644 index 0000000..5c0d8f5 --- /dev/null +++ b/src/MessagingRedisCache/Messaging/DefaultMessagePublisher.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.Options; +using StackExchange.Redis; +using System.Text.Json; + +namespace MessagingRedisCache; + +internal sealed class DefaultMessagePublisher( + IOptions messagingRedisCacheOptionsAccessor) : + IMessagePublisher +{ + public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; set; } = + messagingRedisCacheOptionsAccessor.Value; + + public void Publish( + IConnectionMultiplexer connectionMultiplexer, + RedisKey key) + { + connectionMultiplexer + .GetSubscriber() + .Publish( + new RedisChannel( + MessagingRedisCacheOptions.Channel, + RedisChannel.PatternMode.Literal), + JsonSerializer.Serialize( + new CacheMessage + { + Key = key.ToString(), + PublisherId = MessagingRedisCacheOptions.Id, + }, + SourceGenerationContext.Default.CacheMessage)); + } + + public async Task PublishAsync( + IConnectionMultiplexer connectionMultiplexer, + RedisKey key, + CancellationToken cancellationToken = default) + { + await connectionMultiplexer + .GetSubscriber() + .PublishAsync( + new RedisChannel( + MessagingRedisCacheOptions.Channel, + RedisChannel.PatternMode.Literal), + JsonSerializer.Serialize( + new CacheMessage + { + Key = key.ToString(), + PublisherId = MessagingRedisCacheOptions.Id, + }, + SourceGenerationContext.Default.CacheMessage)) + .ConfigureAwait(false); + } +} diff --git a/src/Messaging/DefaultMessageSubscriber.cs b/src/MessagingRedisCache/Messaging/DefaultMessageSubscriber.cs similarity index 61% rename from src/Messaging/DefaultMessageSubscriber.cs rename to src/MessagingRedisCache/Messaging/DefaultMessageSubscriber.cs index 7453eb1..be7cd7d 100644 --- a/src/Messaging/DefaultMessageSubscriber.cs +++ b/src/MessagingRedisCache/Messaging/DefaultMessageSubscriber.cs @@ -3,20 +3,17 @@ using StackExchange.Redis; using System.Text.Json; -namespace L1L2RedisCache; +namespace MessagingRedisCache; internal class DefaultMessageSubscriber( - IOptions jsonSerializerOptionsAcccessor, - IMemoryCache l1Cache, - IOptions l1L2RedisCacheOptionsOptionsAccessor) : + IMemoryCache memoryCache, + IOptions messagingRedisCacheOptionsAccessor) : IMessageSubscriber { - public JsonSerializerOptions JsonSerializerOptions { get; set; } = - jsonSerializerOptionsAcccessor.Value; - public L1L2RedisCacheOptions L1L2RedisCacheOptions { get; set; } = - l1L2RedisCacheOptionsOptionsAccessor.Value; - public IMemoryCache L1Cache { get; set; } = - l1Cache; + public IMemoryCache MemoryCache { get; set; } = + memoryCache; + public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; set; } = + messagingRedisCacheOptionsAccessor.Value; public EventHandler? OnMessage { get; set; } public EventHandler? OnSubscribe { get; set; } @@ -28,7 +25,7 @@ await connectionMultiplexer .GetSubscriber() .SubscribeAsync( new RedisChannel( - L1L2RedisCacheOptions.Channel, + MessagingRedisCacheOptions.Channel, RedisChannel.PatternMode.Literal), ProcessMessage) .ConfigureAwait(false); @@ -46,7 +43,7 @@ await connectionMultiplexer .GetSubscriber() .UnsubscribeAsync( new RedisChannel( - L1L2RedisCacheOptions.Channel, + MessagingRedisCacheOptions.Channel, RedisChannel.PatternMode.Literal)) .ConfigureAwait(false); } @@ -56,16 +53,14 @@ internal void ProcessMessage( RedisValue message) { var cacheMessage = JsonSerializer - .Deserialize( + .Deserialize( message.ToString(), - JsonSerializerOptions); + SourceGenerationContext.Default.CacheMessage); if (cacheMessage != null && - cacheMessage.PublisherId != L1L2RedisCacheOptions.Id) + cacheMessage.PublisherId != MessagingRedisCacheOptions.Id) { - L1Cache.Remove( - $"{L1L2RedisCacheOptions.KeyPrefix}{cacheMessage.Key}"); - L1Cache.Remove( - $"{L1L2RedisCacheOptions.LockKeyPrefix}{cacheMessage.Key}"); + MemoryCache.Remove( + cacheMessage.Key); OnMessage?.Invoke( this, diff --git a/src/Messaging/IMessagePublisher.cs b/src/MessagingRedisCache/Messaging/IMessagePublisher.cs similarity index 94% rename from src/Messaging/IMessagePublisher.cs rename to src/MessagingRedisCache/Messaging/IMessagePublisher.cs index d0b5458..a8266e6 100644 --- a/src/Messaging/IMessagePublisher.cs +++ b/src/MessagingRedisCache/Messaging/IMessagePublisher.cs @@ -1,6 +1,6 @@ using StackExchange.Redis; -namespace L1L2RedisCache; +namespace MessagingRedisCache; /// /// Publishes messages to other L1L2RedisCache instances indicating cache values have changed. @@ -14,7 +14,7 @@ public interface IMessagePublisher /// The cache key of the value that has changed. void Publish( IConnectionMultiplexer connectionMultiplexer, - string key); + RedisKey key); /// /// Publishes a message indicating a cache value has changed. @@ -25,6 +25,6 @@ void Publish( /// The System.Threading.Tasks.Task that represents the asynchronous operation. Task PublishAsync( IConnectionMultiplexer connectionMultiplexer, - string key, + RedisKey key, CancellationToken cancellationToken = default); } diff --git a/src/Messaging/IMessageSubscriber.cs b/src/MessagingRedisCache/Messaging/IMessageSubscriber.cs similarity index 98% rename from src/Messaging/IMessageSubscriber.cs rename to src/MessagingRedisCache/Messaging/IMessageSubscriber.cs index 069eb9a..7166c9f 100644 --- a/src/Messaging/IMessageSubscriber.cs +++ b/src/MessagingRedisCache/Messaging/IMessageSubscriber.cs @@ -1,6 +1,6 @@ using StackExchange.Redis; -namespace L1L2RedisCache; +namespace MessagingRedisCache; /// /// Subscribes to messages published by other L1L2RedisCache instances indicating cache values have changed. diff --git a/src/Messaging/KeyeventMessageSubscriber.cs b/src/MessagingRedisCache/Messaging/KeyeventMessageSubscriber.cs similarity index 75% rename from src/Messaging/KeyeventMessageSubscriber.cs rename to src/MessagingRedisCache/Messaging/KeyeventMessageSubscriber.cs index c80b6a7..67ca1cb 100644 --- a/src/Messaging/KeyeventMessageSubscriber.cs +++ b/src/MessagingRedisCache/Messaging/KeyeventMessageSubscriber.cs @@ -2,17 +2,17 @@ using Microsoft.Extensions.Options; using StackExchange.Redis; -namespace L1L2RedisCache; +namespace MessagingRedisCache; internal class KeyeventMessageSubscriber( - IMemoryCache l1Cache, - IOptions l1L2RedisCacheOptionsOptionsAccessor) : + IMemoryCache memoryCache, + IOptions messagingRedisCacheOptionsAccessor) : IMessageSubscriber { - public L1L2RedisCacheOptions L1L2RedisCacheOptions { get; set; } = - l1L2RedisCacheOptionsOptionsAccessor.Value; - public IMemoryCache L1Cache { get; set; } = - l1Cache; + public IMemoryCache MemoryCache { get; set; } = + memoryCache; + public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; set; } = + messagingRedisCacheOptionsAccessor.Value; public EventHandler? OnMessage { get; set; } public EventHandler? OnSubscribe { get; set; } @@ -68,15 +68,15 @@ internal void ProcessMessage( RedisChannel channel, RedisValue message) { - if (message.StartsWith( - L1L2RedisCacheOptions.KeyPrefix)) + if (string.IsNullOrEmpty( + MessagingRedisCacheOptions.InstanceName) || + message.StartsWith( + MessagingRedisCacheOptions.InstanceName)) { var key = message - .ToString()[L1L2RedisCacheOptions.KeyPrefix.Length..]; - L1Cache.Remove( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}"); - L1Cache.Remove( - $"{L1L2RedisCacheOptions.LockKeyPrefix}{key}"); + .ToString()[(MessagingRedisCacheOptions.InstanceName?.Length ?? 0)..]; + MemoryCache.Remove( + key); OnMessage?.Invoke( this, diff --git a/src/Messaging/KeyspaceMessageSubscriber.cs b/src/MessagingRedisCache/Messaging/KeyspaceMessageSubscriber.cs similarity index 56% rename from src/Messaging/KeyspaceMessageSubscriber.cs rename to src/MessagingRedisCache/Messaging/KeyspaceMessageSubscriber.cs index a98edbf..239542c 100644 --- a/src/Messaging/KeyspaceMessageSubscriber.cs +++ b/src/MessagingRedisCache/Messaging/KeyspaceMessageSubscriber.cs @@ -1,21 +1,25 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using StackExchange.Redis; +using System.Text.RegularExpressions; -namespace L1L2RedisCache; +namespace MessagingRedisCache; internal class KeyspaceMessageSubscriber( - IMemoryCache l1Cache, - IOptions l1L2RedisCacheOptionsOptionsAccessor) : + IMemoryCache memoryCache, + IOptions messagingRedisCacheOptionsAccessor) : IMessageSubscriber { - public L1L2RedisCacheOptions L1L2RedisCacheOptions { get; set; } = - l1L2RedisCacheOptionsOptionsAccessor.Value; - public IMemoryCache L1Cache { get; set; } = - l1Cache; + public IMemoryCache MemoryCache { get; set; } = + memoryCache; + public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; set; } = + messagingRedisCacheOptionsAccessor.Value; public EventHandler? OnMessage { get; set; } public EventHandler? OnSubscribe { get; set; } + private Regex ChannelPrefixRegex { get; } = new( + @$"__keyspace@\d*__:{messagingRedisCacheOptionsAccessor.Value.InstanceName}"); + public async Task SubscribeAsync( IConnectionMultiplexer connectionMultiplexer, CancellationToken cancellationToken = default) @@ -24,7 +28,7 @@ await connectionMultiplexer .GetSubscriber() .SubscribeAsync( new RedisChannel( - "__keyspace@*__:*", + $"__keyspace@*__:{MessagingRedisCacheOptions.InstanceName}*", RedisChannel.PatternMode.Pattern), ProcessMessage) .ConfigureAwait(false); @@ -42,7 +46,7 @@ await connectionMultiplexer .GetSubscriber() .UnsubscribeAsync( new RedisChannel( - "__keyspace@*__:*", + $"__keyspace@*__:{MessagingRedisCacheOptions.InstanceName}*", RedisChannel.PatternMode.Pattern)) .ConfigureAwait(false); } @@ -54,22 +58,15 @@ internal void ProcessMessage( if (message == "del" || message == "hset") { - var keyPrefixIndex = channel.ToString().IndexOf( - L1L2RedisCacheOptions.KeyPrefix, - StringComparison.Ordinal); - if (keyPrefixIndex != -1) - { - var key = channel.ToString()[ - (keyPrefixIndex + L1L2RedisCacheOptions.KeyPrefix.Length)..]; - L1Cache.Remove( - $"{L1L2RedisCacheOptions.KeyPrefix}{key}"); - L1Cache.Remove( - $"{L1L2RedisCacheOptions.LockKeyPrefix}{key}"); + var key = ChannelPrefixRegex.Replace( + channel.ToString(), + string.Empty); + MemoryCache.Remove( + key); - OnMessage?.Invoke( - this, - new OnMessageEventArgs(key)); - } + OnMessage?.Invoke( + this, + new OnMessageEventArgs(key)); } } } diff --git a/src/Messaging/NopMessagePublisher.cs b/src/MessagingRedisCache/Messaging/NopMessagePublisher.cs similarity index 82% rename from src/Messaging/NopMessagePublisher.cs rename to src/MessagingRedisCache/Messaging/NopMessagePublisher.cs index 7d9fe4e..767bba1 100644 --- a/src/Messaging/NopMessagePublisher.cs +++ b/src/MessagingRedisCache/Messaging/NopMessagePublisher.cs @@ -1,17 +1,17 @@ using StackExchange.Redis; -namespace L1L2RedisCache; +namespace MessagingRedisCache; internal sealed class NopMessagePublisher : IMessagePublisher { public void Publish( IConnectionMultiplexer connectionMultiplexer, - string key) { } + RedisKey key) { } public Task PublishAsync( IConnectionMultiplexer connectionMultiplexer, - string key, + RedisKey key, CancellationToken cancellationToken = default) { return Task.CompletedTask; diff --git a/src/MessagingRedisCache/Messaging/NopMessageSubscriber.cs b/src/MessagingRedisCache/Messaging/NopMessageSubscriber.cs new file mode 100644 index 0000000..07d9bfb --- /dev/null +++ b/src/MessagingRedisCache/Messaging/NopMessageSubscriber.cs @@ -0,0 +1,24 @@ +using StackExchange.Redis; + +namespace MessagingRedisCache; + +internal sealed class NopMessageSubscriber : + IMessageSubscriber +{ + public EventHandler? OnMessage { get; set; } + public EventHandler? OnSubscribe { get; set; } + + public Task SubscribeAsync( + IConnectionMultiplexer connectionMultiplexer, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public Task UnsubscribeAsync( + IConnectionMultiplexer connectionMultiplexer, + CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } +} diff --git a/src/Messaging/OnMessageEventArgs.cs b/src/MessagingRedisCache/Messaging/OnMessageEventArgs.cs similarity index 73% rename from src/Messaging/OnMessageEventArgs.cs rename to src/MessagingRedisCache/Messaging/OnMessageEventArgs.cs index 1e47b63..3458e22 100644 --- a/src/Messaging/OnMessageEventArgs.cs +++ b/src/MessagingRedisCache/Messaging/OnMessageEventArgs.cs @@ -1,4 +1,6 @@ -namespace L1L2RedisCache; +using StackExchange.Redis; + +namespace MessagingRedisCache; /// /// Supplies information about a message event from an IMessageSubscriber. @@ -7,12 +9,11 @@ namespace L1L2RedisCache; /// Initializes a new instance of OnMessageEventArgs. /// public class OnMessageEventArgs( - string key) : + RedisKey key) : EventArgs { - /// /// The cache key pertaining to the message event. /// - public string Key { get; set; } = key; + public RedisKey Key { get; set; } = key; } diff --git a/src/MessagingRedisCache/MessagingRedisCache.cs b/src/MessagingRedisCache/MessagingRedisCache.cs new file mode 100644 index 0000000..eca4460 --- /dev/null +++ b/src/MessagingRedisCache/MessagingRedisCache.cs @@ -0,0 +1,228 @@ +using System.Buffers; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.StackExchangeRedis; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using StackExchange.Redis; + +namespace MessagingRedisCache; + +/// +/// A Redis distributed cache implementation that uses pub/sub. +/// +public class MessagingRedisCache : + RedisCache, + IBufferDistributedCache +{ + /// + /// Initializes a new instance of MessagingRedisCache. + /// + public MessagingRedisCache( + IMessagePublisher messagePublisher, + IMessageSubscriber messageSubscriber, + IMessagingConfigurationVerifier messagingConfigurationVerifier, + IOptions messagingRedisCacheOptionsAccessor, + ILogger? logger = null) : + base(messagingRedisCacheOptionsAccessor) + { + Logger = logger ?? + NullLogger.Instance; + MessagePublisher = messagePublisher ?? + throw new ArgumentNullException( + nameof(messagePublisher)); + MessageSubscriber = messageSubscriber ?? + throw new ArgumentNullException( + nameof(messageSubscriber)); + MessagingConfigurationVerifier = messagingConfigurationVerifier ?? + throw new ArgumentNullException( + nameof(messagingRedisCacheOptionsAccessor)); + MessagingRedisCacheOptions = messagingRedisCacheOptionsAccessor?.Value ?? + throw new ArgumentNullException( + nameof(messagingRedisCacheOptionsAccessor)); + + Database = new Lazy(() => + MessagingRedisCacheOptions + .ConnectionMultiplexerFactory!() + .GetAwaiter() + .GetResult() + .GetDatabase( + MessagingRedisCacheOptions + .ConfigurationOptions? + .DefaultDatabase ?? -1)); + + SubscribeCancellationTokenSource = new CancellationTokenSource(); + _ = SubscribeAsync( + SubscribeCancellationTokenSource.Token); + } + + /// + /// The StackExchange.Redis.IDatabase for the . + /// + public Lazy Database { get; } + + /// + /// Optional. The logger. + /// + public ILogger Logger { get; init; } + + /// + /// The pub/sub publisher. + /// + public IMessagePublisher MessagePublisher { get; } + + /// + /// The pub/sub subscriber. + /// + public IMessageSubscriber MessageSubscriber { get; } + + /// + /// The messaging configuration verifier. + /// + public IMessagingConfigurationVerifier MessagingConfigurationVerifier { get; } + + /// + /// Configuration options. + /// + public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; } + + private bool IsDisposed { get; set; } + private CancellationTokenSource SubscribeCancellationTokenSource { get; set; } + + /// + public new void Dispose() + { + base.Dispose(); + Dispose(true); + } + + /// + public new void Remove(string key) + { + base.Remove(key); + MessagePublisher.Publish( + Database.Value.Multiplexer, + key); + } + + /// + public new async Task RemoveAsync( + string key, + CancellationToken token = default) + { + await base.RemoveAsync( + key, + token) + .ConfigureAwait(false); + await MessagePublisher + .PublishAsync( + Database.Value.Multiplexer, + key, + token) + .ConfigureAwait(false); + } + + /// + public new void Set( + string key, + byte[] value, + DistributedCacheEntryOptions options) + { + base.Set( + key, + value, + options); + MessagePublisher.Publish( + Database.Value.Multiplexer, + key); + } + + /// + public void Set( + string key, + ReadOnlySequence value, + DistributedCacheEntryOptions options) + { + ((IBufferDistributedCache)this).Set(key, value, options); + MessagePublisher.Publish( + Database.Value.Multiplexer, + key); + } + + /// + public new async Task SetAsync( + string key, + byte[] value, + DistributedCacheEntryOptions options, + CancellationToken token = default) + { + await base.SetAsync( + key, + value, + options, + token) + .ConfigureAwait(false); + await MessagePublisher + .PublishAsync( + Database.Value.Multiplexer, + key, + token) + .ConfigureAwait(false); + } + + private void Dispose(bool isDisposing) + { + if (IsDisposed) + { + return; + } + + if (isDisposing) + { + SubscribeCancellationTokenSource.Dispose(); + } + + IsDisposed = true; + } + + private async Task SubscribeAsync( + CancellationToken cancellationToken = default) + { + while (true) + { + try + { + if (!await MessagingConfigurationVerifier + .VerifyConfigurationAsync( + Database.Value, + cancellationToken) + .ConfigureAwait(false)) + { + Logger.MessagingConfigurationInvalid( + MessagingRedisCacheOptions.MessagingType); + } + + await MessageSubscriber + .SubscribeAsync( + Database.Value.Multiplexer, + cancellationToken) + .ConfigureAwait(false); + break; + } + catch (RedisConnectionException redisConnectionException) + { + Logger.SubscriberFailed( + MessagingRedisCacheOptions + .SubscriberRetryDelay, + redisConnectionException); + + await Task + .Delay( + MessagingRedisCacheOptions + .SubscriberRetryDelay, + cancellationToken) + .ConfigureAwait(false); + } + } + } +} diff --git a/src/MessagingRedisCache/MessagingRedisCache.csproj b/src/MessagingRedisCache/MessagingRedisCache.csproj new file mode 100644 index 0000000..3fe3e21 --- /dev/null +++ b/src/MessagingRedisCache/MessagingRedisCache.csproj @@ -0,0 +1,21 @@ + + + true + CA1724; + README.md + true + net8.0;net9.0 + + + + + + + + + + + + + + diff --git a/src/L1L2RedisCacheLoggerExtensions.cs b/src/MessagingRedisCache/MessagingRedisCacheLoggerExtensions.cs similarity index 75% rename from src/L1L2RedisCacheLoggerExtensions.cs rename to src/MessagingRedisCache/MessagingRedisCacheLoggerExtensions.cs index cd66f2e..da0942f 100644 --- a/src/L1L2RedisCacheLoggerExtensions.cs +++ b/src/MessagingRedisCache/MessagingRedisCacheLoggerExtensions.cs @@ -1,14 +1,14 @@ using Microsoft.Extensions.Logging; -namespace L1L2RedisCache; +namespace MessagingRedisCache; -internal static partial class L1L2RedisCacheLoggerExtensions +internal static partial class MessagingRedisCacheLoggerExtensions { [LoggerMessage( Level = LogLevel.Error, Message = "Redis notify-keyspace-events config is invalid for MessagingType {MessagingType}")] public static partial void MessagingConfigurationInvalid( - this ILogger logger, + this ILogger logger, MessagingType messagingType, Exception? exception = null); @@ -16,7 +16,7 @@ public static partial void MessagingConfigurationInvalid( Level = LogLevel.Error, Message = "Failed to initialize subscriber; retrying in {SubscriberRetryDelay}")] public static partial void SubscriberFailed( - this ILogger logger, + this ILogger logger, TimeSpan subscriberRetryDelay, Exception? exception = null); } diff --git a/src/MessagingRedisCache/MessagingRedisCacheOptions.cs b/src/MessagingRedisCache/MessagingRedisCacheOptions.cs new file mode 100644 index 0000000..f90a250 --- /dev/null +++ b/src/MessagingRedisCache/MessagingRedisCacheOptions.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Caching.StackExchangeRedis; + +namespace MessagingRedisCache; + +/// +/// Configuration options for L1L2RedisCache. +/// +public class MessagingRedisCacheOptions() : + RedisCacheOptions() +{ + /// + /// The pub/sub channel name. + /// + public string Channel => + $"{InstanceName}{nameof(MessagingRedisCache)}"; + + /// + /// Unique identifier for the operating instance. + /// + public Guid Id { get; } = + Guid.NewGuid(); + + /// + /// The type of messaging to use for L1 memory cache eviction. + /// + public MessagingType MessagingType { get; set; } = + MessagingType.Default; + + /// + /// The duration of time to delay before retrying subscriber intialization. + /// + public TimeSpan SubscriberRetryDelay { get; set; } = + TimeSpan.FromSeconds(5); +} diff --git a/src/MessagingType.cs b/src/MessagingRedisCache/MessagingType.cs similarity index 97% rename from src/MessagingType.cs rename to src/MessagingRedisCache/MessagingType.cs index 97bd3e2..e79f451 100644 --- a/src/MessagingType.cs +++ b/src/MessagingRedisCache/MessagingType.cs @@ -1,4 +1,4 @@ -namespace L1L2RedisCache; +namespace MessagingRedisCache; /// /// The type of messaging system to use for L1 memory cache eviction. diff --git a/src/MessagingRedisCache/README.md b/src/MessagingRedisCache/README.md new file mode 100644 index 0000000..6db442a --- /dev/null +++ b/src/MessagingRedisCache/README.md @@ -0,0 +1,53 @@ +# MessagingRedisCache + +`MessagingRedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) based on [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs). It will utilize [Redis pub/sub](https://redis.io/topics/pubsub) to ensure that cache entries can be synchronized in a distributed system, where direct deferral to Redis is not always performant. Because of this, it is a valuable backing store for [HybridCache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid), and is used as the `IDistributedCache` for [`L1L2RedisCache`](lol.com). + +All changes to entries in `MessagingRedisCache` by way of removal or updates, will result in a message being published. The default implementation is to innately publish these messages. Alternatively, [keyevent notifications](https://redis.io/topics/notifications) or [keyspace notifications](https://redis.io/topics/notifications) can be used if the Redis server is configured for them. + +I expect to gracefully decomission this project when [`StackExchange.Redis`](https://github.com/StackExchange/StackExchange.Redis) has [client-side caching](https://redis.io/docs/latest/develop/use/client-side-caching/) support. + +## Configuration + +It is intended that `MessagingRedisCache` be used in conjunction with another kind of layered caching solution, specificially [HybridCache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) or [`L1L2RedisCache`](lol.com). Without any kind of layered cache solution, `MessagingRedisCache` will only publish Redis pub/sub messages for another consumer. + +`MessagingRedisCache` can be registered during startup with the following `IServiceCollection` extension method: + +``` +services + .AddMessagingRedisCache(options => + { + options.Configuration = "redis"; + options.InstanceName = "Namespace:Prefix:"; + }); +``` + +### Message Subscription + +A message subscriber can be registered on the `IMessagingRedisCacheBuilder` which will automatically evict cache entries from an `IMemoryCache` registered on the `IServiceCollection`: + +``` +services + .AddMessagingRedisCache(options => + { + options.Configuration = "redis"; + options.InstanceName = "Namespace:Prefix:"; + }) + .AddMemoryCacheSubscriber(); +``` + +This should be done explicitly when using [HybridCache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) and is done automatically when using [`L1L2RedisCache`](lol.com). + +## MessagingRedisCacheOptions + +`MessagingRedisCacheOptions` are an extension of the standard `RedisCache` [`RedisCacheOptions`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCacheOptions.cs). The following additional customizations are supported: + +### MessagingType + +The type of message publishing system to use: + +| MessagingType | Description | Suggestion | +| - | - | - | +| `Default` | Publish standard `MessagingRedisCache` [pub/sub](https://redis.io/topics/pubsub) messages. | Default behavior. The Redis server requires no additional configuration. | +| `KeyeventNotifications` | Rely on [keyevent notifications](https://redis.io/topics/notifications) for message publishing instead of standard `MessagingRedisCache` [pub/sub](https://redis.io/topics/pubsub) messages. The Redis server must have keyevent notifications enabled. | This is only advisable if the Redis server is already using [keyevent notifications](https://redis.io/topics/notifications) with at least a `ghE` configuration and the majority of keys in the server are managed by `MessagingRedisCache`. | +| `KeyspaceNotifications` | Rely on [keyspace notifications](https://redis.io/topics/notifications) for message publishing instead of standard `MessagingRedisCache` [pub/sub](https://redis.io/topics/pubsub) messages. The Redis server must have keyspace notifications enabled. | This is only advisable if the Redis server is already using [keyevent notifications](https://redis.io/topics/notifications) with at least a `ghK` configuration and the majority of keys in the server are managed by `MessagingRedisCache`. | + diff --git a/src/MessagingRedisCache/SourceGenerationContext.cs b/src/MessagingRedisCache/SourceGenerationContext.cs new file mode 100644 index 0000000..ce46601 --- /dev/null +++ b/src/MessagingRedisCache/SourceGenerationContext.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace MessagingRedisCache; + +[JsonSerializable(typeof(CacheMessage))] +[JsonSerializable(typeof(Guid))] +[JsonSerializable(typeof(string))] +internal partial class SourceGenerationContext : + JsonSerializerContext +{ +} \ No newline at end of file diff --git a/tests/System/Benchmark/BenchmarkBase.cs b/tests/L1L2RedisCache/System/Benchmark/BenchmarkBase.cs similarity index 84% rename from tests/System/Benchmark/BenchmarkBase.cs rename to tests/L1L2RedisCache/System/Benchmark/BenchmarkBase.cs index 3cdd3b6..879a9d3 100644 --- a/tests/System/Benchmark/BenchmarkBase.cs +++ b/tests/L1L2RedisCache/System/Benchmark/BenchmarkBase.cs @@ -1,8 +1,11 @@ using BenchmarkDotNet.Attributes; +using MessagingRedisCache; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace L1L2RedisCache.Tests.System; @@ -10,7 +13,7 @@ public abstract class BenchmarkBase { [Params(100)] public int Iterations { get; set; } - + protected DistributedCacheEntryOptions DistributedCacheEntryOptions { get; set; } = new DistributedCacheEntryOptions { @@ -26,7 +29,6 @@ public void GlobalSetup() { var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") - .AddEnvironmentVariables() .Build(); var services = new ServiceCollection(); @@ -42,8 +44,8 @@ public void GlobalSetup() .GetRequiredService(); L1L2Cache = serviceProvider .GetRequiredService(); - L2Cache = serviceProvider - .GetRequiredService>() - .Invoke(); + L2Cache = new RedisCache( + serviceProvider + .GetRequiredService>()); } } \ No newline at end of file diff --git a/tests/System/Benchmark/GetBenchmark.cs b/tests/L1L2RedisCache/System/Benchmark/GetBenchmark.cs similarity index 98% rename from tests/System/Benchmark/GetBenchmark.cs rename to tests/L1L2RedisCache/System/Benchmark/GetBenchmark.cs index f2cf0c8..fbf4ade 100644 --- a/tests/System/Benchmark/GetBenchmark.cs +++ b/tests/L1L2RedisCache/System/Benchmark/GetBenchmark.cs @@ -44,7 +44,7 @@ public void L1Get() iteration <= Iterations; iteration++) { - L1Cache!.Get( + L1Cache!.Get( $"Get:{iteration}"); } } diff --git a/tests/System/Benchmark/SetBenchmark.cs b/tests/L1L2RedisCache/System/Benchmark/SetBenchmark.cs similarity index 100% rename from tests/System/Benchmark/SetBenchmark.cs rename to tests/L1L2RedisCache/System/Benchmark/SetBenchmark.cs diff --git a/tests/L1L2RedisCache/System/L1L2RedisCache.Tests.System.csproj b/tests/L1L2RedisCache/System/L1L2RedisCache.Tests.System.csproj new file mode 100644 index 0000000..b144620 --- /dev/null +++ b/tests/L1L2RedisCache/System/L1L2RedisCache.Tests.System.csproj @@ -0,0 +1,24 @@ + + + false + CA1515; + net9.0 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/System/MessagingTests.cs b/tests/L1L2RedisCache/System/MessagingTests.cs similarity index 84% rename from tests/System/MessagingTests.cs rename to tests/L1L2RedisCache/System/MessagingTests.cs index 7ab75da..a1acb68 100644 --- a/tests/System/MessagingTests.cs +++ b/tests/L1L2RedisCache/System/MessagingTests.cs @@ -1,4 +1,7 @@ +using System.Text; +using MessagingRedisCache; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -13,7 +16,6 @@ public MessagingTests() { Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") - .AddEnvironmentVariables() .Build(); EventTimeout = TimeSpan.FromSeconds(5); } @@ -41,8 +43,8 @@ public async Task MessagingTypeTest( var primaryL1L2Cache = primaryServiceProvider .GetRequiredService(); - var primaryL1L2CacheOptions = primaryServiceProvider - .GetRequiredService>() + var primaryMessagingRedisCacheOptions = primaryServiceProvider + .GetRequiredService>() .Value; await SetAndVerifyConfigurationAsync( @@ -73,6 +75,8 @@ await SetAndVerifyConfigurationAsync( subscribeAutoResetEvent.Set(); }; + var secondaryL1Cache = secondaryServiceProvider + .GetRequiredService(); var secondaryL1L2Cache = secondaryServiceProvider .GetRequiredService(); @@ -93,11 +97,17 @@ await primaryL1L2Cache Assert.IsTrue( messageAutoResetEvent .WaitOne(EventTimeout)); + Assert.IsNull( + secondaryL1Cache + .Get(key)); Assert.AreEqual( value, await secondaryL1L2Cache .GetStringAsync(key) .ConfigureAwait(false)); + Assert.IsTrue( + Encoding.ASCII.GetBytes(value).SequenceEqual( + secondaryL1Cache.Get(key) ?? [ ])); // L1 eviction via set // L1 population via L2 @@ -108,11 +118,17 @@ await primaryL1L2Cache Assert.IsTrue( messageAutoResetEvent .WaitOne(EventTimeout)); + Assert.IsNull( + secondaryL1Cache + .Get(key)); Assert.AreEqual( value, await secondaryL1L2Cache .GetStringAsync(key) .ConfigureAwait(false)); + Assert.IsTrue( + Encoding.ASCII.GetBytes(value).SequenceEqual( + secondaryL1Cache.Get(key) ?? [ ])); // L1 eviction via remove await primaryL1L2Cache @@ -121,6 +137,9 @@ await primaryL1L2Cache Assert.IsTrue( messageAutoResetEvent .WaitOne(EventTimeout)); + Assert.IsNull( + secondaryL1Cache + .Get(key)); Assert.IsNull( await secondaryL1L2Cache .GetStringAsync(key) @@ -152,4 +171,4 @@ await configurationVerifier l1L2Cache.Database.Value) .ConfigureAwait(false)); } -} +} \ No newline at end of file diff --git a/tests/System/PerformanceTests.cs b/tests/L1L2RedisCache/System/PerformanceTests.cs similarity index 79% rename from tests/System/PerformanceTests.cs rename to tests/L1L2RedisCache/System/PerformanceTests.cs index 167eafa..eb221be 100644 --- a/tests/System/PerformanceTests.cs +++ b/tests/L1L2RedisCache/System/PerformanceTests.cs @@ -40,14 +40,14 @@ public void GetPerformanceTest() l1L2GetReport.ResultStatistics?.Median; Assert.IsTrue( l2GetVsL1L2GetRatio > 100, - $"L1L2Cache Get must perform significantly better (> 100) than RedisCache Get: {l2GetVsL1L2GetRatio}"); + $"L1L2RedisCache Get must perform significantly better (> 100) than RedisCache Get: {l2GetVsL1L2GetRatio}"); var l1L2GetPropagationVsl2GetRatio = - l2GetReport.ResultStatistics?.Median / - l1L2GetPropagationReport.ResultStatistics?.Median; + l1L2GetPropagationReport.ResultStatistics?.Median / + l2GetReport.ResultStatistics?.Median; Assert.IsTrue( - l1L2GetPropagationVsl2GetRatio > 3, - $"L1L2Cache Get must perform better (> 3) than to RedisCache Get: {l1L2GetPropagationVsl2GetRatio}"); + l1L2GetPropagationVsl2GetRatio < 3, + $"L1L2RedisCache GetPropagation cannot perform significantly worse (< 3) than RedisCache Get: {l1L2GetPropagationVsl2GetRatio}"); } [TestMethod] @@ -72,6 +72,6 @@ public void SetPerformanceTest() l2SetReport.ResultStatistics?.Median; Assert.IsTrue( l1L2SetVsl2SetRatio < 3, - $"L1L2Cache Set cannot perform significantly worse (< 3) than RedisCache Set: {l1L2SetVsl2SetRatio}"); + $"L1L2RedisCache Set cannot perform significantly worse (< 3) than RedisCache Set: {l1L2SetVsl2SetRatio}"); } -} +} \ No newline at end of file diff --git a/tests/System/ReliabilityTests.cs b/tests/L1L2RedisCache/System/ReliabilityTests.cs similarity index 97% rename from tests/System/ReliabilityTests.cs rename to tests/L1L2RedisCache/System/ReliabilityTests.cs index 7885020..fb254ee 100644 --- a/tests/System/ReliabilityTests.cs +++ b/tests/L1L2RedisCache/System/ReliabilityTests.cs @@ -1,3 +1,4 @@ +using MessagingRedisCache; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -13,7 +14,6 @@ public ReliabilityTests() { Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") - .AddEnvironmentVariables() .Build(); EventTimeout = TimeSpan.FromSeconds(5); } @@ -52,4 +52,4 @@ public void InitializeBadConnectionTest() () => l1L2Cache .GetStringAsync(string.Empty)); } -} +} \ No newline at end of file diff --git a/tests/System/appsettings.json b/tests/L1L2RedisCache/System/appsettings.json similarity index 57% rename from tests/System/appsettings.json rename to tests/L1L2RedisCache/System/appsettings.json index 80298ea..22f31f2 100644 --- a/tests/System/appsettings.json +++ b/tests/L1L2RedisCache/System/appsettings.json @@ -2,6 +2,6 @@ "L1L2RedisCache": { "Configuration": "redis", - "InstanceName": "L1L2RedisCache:Test:" + "InstanceName": "MessagingRedisCache:Test:" } } \ No newline at end of file diff --git a/tests/MessagingRedisCache/System/DefaultMessagingTests.cs b/tests/MessagingRedisCache/System/DefaultMessagingTests.cs new file mode 100644 index 0000000..6afce20 --- /dev/null +++ b/tests/MessagingRedisCache/System/DefaultMessagingTests.cs @@ -0,0 +1,15 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MessagingRedisCache.Tests.System; + +[TestClass] +public class DefaultMessagingTests() : + MessagingTestsBase( + MessagingType.Default) +{ + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + } +} diff --git a/tests/MessagingRedisCache/System/HybridCacheTests.cs b/tests/MessagingRedisCache/System/HybridCacheTests.cs new file mode 100644 index 0000000..2b36dc2 --- /dev/null +++ b/tests/MessagingRedisCache/System/HybridCacheTests.cs @@ -0,0 +1,106 @@ +using Microsoft.Extensions.Caching.Hybrid; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MessagingRedisCache.Tests.System; + +[TestClass] +public class HybridCacheTests : + TestsBase +{ + public HybridCacheTests() : + base(MessagingType.Default) + { + PrimaryServices.AddHybridCache(); + SecondaryServices.AddHybridCache(); + } + + public HybridCacheEntryOptions HybridCacheEntryOptions { get; } = + new HybridCacheEntryOptions + { + Expiration = TimeSpan.FromHours(1), + }; + public HybridCache PrimaryHybridCache => + PrimaryServiceProvider + .GetRequiredService(); + public HybridCache SecondaryHybridCache => + SecondaryServiceProvider + .GetRequiredService(); + + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + } + + [TestMethod] + public async Task GetOrCreateAsyncTest() + { + // await SetAndVerifyConfigurationAsync() + // .ConfigureAwait(false); + + var secondaryMessageSubscriber = SecondaryServiceProvider + .GetRequiredService(); + using var messageAutoResetEvent = new AutoResetEvent(false); + secondaryMessageSubscriber.OnMessage += (sender, e) => + { + messageAutoResetEvent.Set(); + }; + + for (var iteration = 0; iteration < Iterations; iteration++) + { + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + await PrimaryHybridCache + .SetAsync( + key, + value, + HybridCacheEntryOptions) + .ConfigureAwait(false); + var test = await PrimaryDistributedCache.GetAsync(key).ConfigureAwait(false); + Assert.IsTrue( + messageAutoResetEvent + .WaitOne(EventTimeout)); + Assert.AreEqual( + value, + await PrimaryHybridCache + .GetOrCreateAsync( + key, + cancellationToken => + ValueTask.FromResult(null), + HybridCacheEntryOptions) + .ConfigureAwait(false)); + Assert.IsNull( + SecondaryMemoryCache + .Get(key)); + Assert.AreEqual( + value, + await SecondaryHybridCache + .GetOrCreateAsync( + key, + cancellationToken => + ValueTask.FromResult(null), + HybridCacheEntryOptions) + .ConfigureAwait(false)); + Assert.IsNotNull( + SecondaryMemoryCache + .Get(key)); + + await PrimaryHybridCache + .RemoveAsync( + key) + .ConfigureAwait(false); + Assert.IsTrue( + messageAutoResetEvent + .WaitOne(EventTimeout)); + Assert.IsNull( + PrimaryMemoryCache + .Get(key)); + Assert.IsNull( + SecondaryMemoryCache + .Get(key)); + } + } +} diff --git a/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs b/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs new file mode 100644 index 0000000..af326d3 --- /dev/null +++ b/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs @@ -0,0 +1,15 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MessagingRedisCache.Tests.System; + +[TestClass] +public class KeyeventMessagingTests() : + MessagingTestsBase( + MessagingType.KeyeventNotifications) +{ + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + } +} diff --git a/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs b/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs new file mode 100644 index 0000000..7e1ea98 --- /dev/null +++ b/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs @@ -0,0 +1,15 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MessagingRedisCache.Tests.System; + +[TestClass] +public class KeyspaceMessagingTests() : + MessagingTestsBase( + MessagingType.KeyspaceNotifications) +{ + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + } +} diff --git a/tests/MessagingRedisCache/System/MessagingRedisCache.Tests.System.csproj b/tests/MessagingRedisCache/System/MessagingRedisCache.Tests.System.csproj new file mode 100644 index 0000000..6f8e026 --- /dev/null +++ b/tests/MessagingRedisCache/System/MessagingRedisCache.Tests.System.csproj @@ -0,0 +1,24 @@ + + + false + CA1515;EXTEXP0018; + net9.0 + + + + + + + + + + + + + + + + + + + diff --git a/tests/MessagingRedisCache/System/MessagingTestsBase.cs b/tests/MessagingRedisCache/System/MessagingTestsBase.cs new file mode 100644 index 0000000..a30c15d --- /dev/null +++ b/tests/MessagingRedisCache/System/MessagingTestsBase.cs @@ -0,0 +1,176 @@ +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Diagnostics.CodeAnalysis; +using System.Text; + +namespace MessagingRedisCache.Tests.System; + +[TestClass] +public abstract class MessagingTestsBase( + MessagingType messagingType) : + TestsBase( + messagingType) +{ + [TestInitialize] + public override void TestInitialize() + { + base.TestInitialize(); + } + + [TestMethod] + public async Task RemoveAsyncTest() + { + await SetAndVerifyConfigurationAsync() + .ConfigureAwait(false); + + var secondaryMessageSubscriber = SecondaryServiceProvider + .GetRequiredService(); + using var messageAutoResetEvent = new AutoResetEvent(false); + secondaryMessageSubscriber.OnMessage += (sender, e) => + { + messageAutoResetEvent.Set(); + }; + + for (var iteration = 0; iteration < Iterations; iteration++) + { + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + await PrimaryDistributedCache + .SetStringAsync(key, value) + .ConfigureAwait(false); + Assert.IsTrue( + messageAutoResetEvent + .WaitOne(EventTimeout)); + + SecondaryMemoryCache + .Set(key, value); + await PrimaryDistributedCache + .RemoveAsync( + key) + .ConfigureAwait(false); + Assert.IsTrue( + messageAutoResetEvent + .WaitOne(EventTimeout)); + Assert.IsNull( + SecondaryMemoryCache + .Get(key)); + } + } + + [TestMethod] + [SuppressMessage("Performance", "CA1849")] + public async Task RemoveTest() + { + await SetAndVerifyConfigurationAsync() + .ConfigureAwait(false); + + var secondaryMessageSubscriber = SecondaryServiceProvider + .GetRequiredService(); + using var messageAutoResetEvent = new AutoResetEvent(false); + secondaryMessageSubscriber.OnMessage += (sender, e) => + { + messageAutoResetEvent.Set(); + }; + + for (var iteration = 0; iteration < Iterations; iteration++) + { + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + await PrimaryDistributedCache + .SetStringAsync(key, value) + .ConfigureAwait(false); + Assert.IsTrue( + messageAutoResetEvent + .WaitOne(EventTimeout)); + + SecondaryMemoryCache + .Set(key, value); + PrimaryDistributedCache + .Remove( + key); + Assert.IsTrue( + messageAutoResetEvent + .WaitOne(EventTimeout)); + Assert.IsNull( + SecondaryMemoryCache + .Get(key)); + } + } + + [TestMethod] + public async Task SetAsyncTest() + { + await SetAndVerifyConfigurationAsync() + .ConfigureAwait(false); + + var secondaryMessageSubscriber = SecondaryServiceProvider + .GetRequiredService(); + using var messageAutoResetEvent = new AutoResetEvent(false); + secondaryMessageSubscriber.OnMessage += (sender, e) => + { + messageAutoResetEvent.Set(); + }; + + for (var iteration = 0; iteration < Iterations; iteration++) + { + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + SecondaryMemoryCache + .Set(key, value); + await PrimaryDistributedCache + .SetStringAsync( + key, + value, + DistributedCacheEntryOptions) + .ConfigureAwait(false); + Assert.IsTrue( + messageAutoResetEvent + .WaitOne(EventTimeout)); + Assert.IsNull( + SecondaryMemoryCache + .Get(key)); + } + } + + [TestMethod] + [SuppressMessage("Performance", "CA1849")] + public async Task SetTest() + { + await SetAndVerifyConfigurationAsync() + .ConfigureAwait(false); + + var secondaryMessageSubscriber = SecondaryServiceProvider + .GetRequiredService(); + using var messageAutoResetEvent = new AutoResetEvent(false); + secondaryMessageSubscriber.OnMessage += (sender, e) => + { + messageAutoResetEvent.Set(); + }; + + for (var iteration = 0; iteration < Iterations; iteration++) + { + var key = Guid.NewGuid().ToString(); + var value = Guid.NewGuid().ToString(); + + SecondaryMemoryCache + .Set(key, value); + PrimaryDistributedCache + .Set( + key, + Encoding.ASCII.GetBytes(value), + DistributedCacheEntryOptions); + Assert.IsTrue( + messageAutoResetEvent + .WaitOne(EventTimeout)); + Assert.IsNull( + SecondaryMemoryCache + .Get(key)); + } + } +} diff --git a/tests/MessagingRedisCache/System/TestsBase.cs b/tests/MessagingRedisCache/System/TestsBase.cs new file mode 100644 index 0000000..5430b5f --- /dev/null +++ b/tests/MessagingRedisCache/System/TestsBase.cs @@ -0,0 +1,104 @@ +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace MessagingRedisCache.Tests.System; + +[TestClass] +public abstract class TestsBase +{ + internal TestsBase( + MessagingType messagingType) + { + MessagingType = messagingType; + + PrimaryServices = new ServiceCollection(); + PrimaryServices.AddSingleton(Configuration); + PrimaryServices.AddMemoryCache(); + PrimaryServices + .AddMessagingRedisCache(options => + { + Configuration.Bind("MessagingRedisCache", options); + options.MessagingType = MessagingType; + }) + .AddMemoryCacheSubscriber();; + + SecondaryServices = new ServiceCollection(); + SecondaryServices.AddSingleton(Configuration); + SecondaryServices.AddMemoryCache(); + SecondaryServices + .AddMessagingRedisCache(options => + { + Configuration.Bind("MessagingRedisCache", options); + options.MessagingType = MessagingType; + }) + .AddMemoryCacheSubscriber(); + } + + public IConfiguration Configuration { get; } = + new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build(); + public DistributedCacheEntryOptions DistributedCacheEntryOptions { get; } = + new DistributedCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = + TimeSpan.FromHours(1), + }; + public TimeSpan EventTimeout { get; } = + TimeSpan.FromSeconds(10); + public int Iterations { get; } = 100; + public MessagingType MessagingType { get; } + public IDistributedCache PrimaryDistributedCache { get; private set; } = default!; + public IMemoryCache PrimaryMemoryCache { get; private set; } = default!; + public IServiceProvider PrimaryServiceProvider { get; private set; } = default!; + public IDistributedCache SecondaryDistributedCache { get; private set; } = default!; + public IMemoryCache SecondaryMemoryCache { get; private set; } = default!; + public IServiceProvider SecondaryServiceProvider { get; private set; } = default!; + + protected IServiceCollection PrimaryServices { get; private set; } = default!; + protected IServiceCollection SecondaryServices { get; private set; } = default!; + + [TestInitialize] + public virtual void TestInitialize() + { + PrimaryServiceProvider = PrimaryServices + .BuildServiceProvider(); + PrimaryDistributedCache = PrimaryServiceProvider + .GetRequiredService(); + PrimaryMemoryCache = PrimaryServiceProvider + .GetRequiredService(); + + SecondaryServiceProvider = SecondaryServices + .BuildServiceProvider(); + SecondaryDistributedCache = SecondaryServiceProvider + .GetRequiredService(); + SecondaryMemoryCache = SecondaryServiceProvider + .GetRequiredService(); + } + + public async Task SetAndVerifyConfigurationAsync() + { + var messagingRedisCache = PrimaryServiceProvider + .GetRequiredService() as MessagingRedisCache; + + await messagingRedisCache!.Database.Value + .ExecuteAsync( + "config", + "set", + "notify-keyspace-events", + MessagingConfigurationVerifier + .NotifyKeyspaceEventsConfig[MessagingType]) + .ConfigureAwait(false); + + var configurationVerifier = PrimaryServiceProvider + .GetRequiredService(); + Assert.IsTrue( + await configurationVerifier + .VerifyConfigurationAsync( + messagingRedisCache.Database.Value) + .ConfigureAwait(false)); + } +} diff --git a/tests/MessagingRedisCache/System/appsettings.json b/tests/MessagingRedisCache/System/appsettings.json new file mode 100644 index 0000000..aec495d --- /dev/null +++ b/tests/MessagingRedisCache/System/appsettings.json @@ -0,0 +1,7 @@ +{ + "MessagingRedisCache": + { + "Configuration": "redis", + "InstanceName": "MessagingRedisCache:Test:" + } +} \ No newline at end of file diff --git a/tests/MessagingRedisCache/Unit/MessagingRedisCache.Tests.Unit.csproj b/tests/MessagingRedisCache/Unit/MessagingRedisCache.Tests.Unit.csproj new file mode 100644 index 0000000..27a0fcd --- /dev/null +++ b/tests/MessagingRedisCache/Unit/MessagingRedisCache.Tests.Unit.csproj @@ -0,0 +1,17 @@ + + + false + CA1515; + net9.0 + + + + + + + + + + + + diff --git a/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs b/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs new file mode 100644 index 0000000..434f098 --- /dev/null +++ b/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs @@ -0,0 +1,138 @@ +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using StackExchange.Redis; + +namespace MessagingRedisCache.Tests.Unit; + +[TestClass] +public class MessagingRedisCacheTests +{ + public MessagingRedisCacheTests() + { + MessagingRedisCacheOptions = new MessagingRedisCacheOptions + { + InstanceName = "L1L2RedisCache:Test:", + }; + + var database = Substitute + .For(); + + ConnectionMultiplexer = Substitute + .For(); + ConnectionMultiplexer + .GetDatabase( + Arg.Any(), + Arg.Any()) + .Returns(database); + + MessagingRedisCacheOptions.ConnectionMultiplexerFactory = + () => Task.FromResult(ConnectionMultiplexer); + + MessagePublisher = Substitute + .For(); + MessagePublisher + .Publish( + Arg.Any(), + Arg.Any()); + MessagePublisher + .PublishAsync( + Arg.Any(), + Arg.Any(), + Arg.Any()); + + MessageSubscriber = Substitute + .For(); + MessageSubscriber + .SubscribeAsync( + Arg.Any(), + Arg.Any()); + + var messagingConfigurationVerifier = Substitute + .For(); + messagingConfigurationVerifier + .VerifyConfigurationAsync( + Arg.Any(), + Arg.Any()); + + MessagingRedisCache = new MessagingRedisCache( + MessagePublisher, + MessageSubscriber, + messagingConfigurationVerifier, + Options.Create(MessagingRedisCacheOptions)); + } + + public IConnectionMultiplexer ConnectionMultiplexer { get; } + public IMessagePublisher MessagePublisher { get; } + public IMessageSubscriber MessageSubscriber { get; } + public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; } + public MessagingRedisCache MessagingRedisCache { get; } + + [TestMethod] + public async Task RemoveAsyncTest() + { + var key = "key"; + + await MessagingRedisCache + .RemoveAsync(key) + .ConfigureAwait(false); + + await MessagePublisher + .Received() + .PublishAsync( + Arg.Any(), + key) + .ConfigureAwait(false); + } + + [TestMethod] + public void RemoveTest() + { + var key = "key"; + + MessagingRedisCache + .Remove(key); + + MessagePublisher + .Received() + .Publish( + Arg.Any(), + key); + } + + [TestMethod] + public async Task SetAsyncTest() + { + var key = "key"; + var value = " "u8.ToArray(); + + await MessagingRedisCache + .SetAsync(key, value) + .ConfigureAwait(false); + + await MessagePublisher + .Received() + .PublishAsync( + Arg.Any(), + key) + .ConfigureAwait(false); + } + + [TestMethod] + public void SetTest() + { + var key = "key"; + var value = " "u8.ToArray(); + + MessagingRedisCache + .Set(key, value); + + MessagePublisher + .Received() + .Publish( + Arg.Any(), + key); + } +} diff --git a/tests/System/L1L2RedisCache.Tests.System.csproj b/tests/System/L1L2RedisCache.Tests.System.csproj deleted file mode 100644 index 1d820c2..0000000 --- a/tests/System/L1L2RedisCache.Tests.System.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - latest-recommended - true - enable - false - enable - net9.0 - - - - - - - - - - - - - - - - - - - - diff --git a/tests/Unit/L1L2RedisCache.Tests.Unit.csproj b/tests/Unit/L1L2RedisCache.Tests.Unit.csproj deleted file mode 100644 index 0a49c3d..0000000 --- a/tests/Unit/L1L2RedisCache.Tests.Unit.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - latest-recommended - true - enable - false - enable - net9.0 - - - - - - - - - - - - diff --git a/tests/Unit/L1L2RedisCacheTests.cs b/tests/Unit/L1L2RedisCacheTests.cs deleted file mode 100644 index 4f114fe..0000000 --- a/tests/Unit/L1L2RedisCacheTests.cs +++ /dev/null @@ -1,282 +0,0 @@ -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using NSubstitute; -using StackExchange.Redis; - -namespace L1L2RedisCache.Tests.Unit; - -[TestClass] -public class L1L2RedisCacheTests -{ - public L1L2RedisCacheTests() - { - L1Cache = new MemoryCache( - Options.Create(new MemoryCacheOptions())); - - L2Cache = new MemoryDistributedCache( - Options.Create( - new MemoryDistributedCacheOptions())); - - L1L2RedisCacheOptions = Options - .Create( - new L1L2RedisCacheOptions - { - InstanceName = "L1L2RedisCache:Test:", - }) - .Value; - - var database = Substitute - .For(); - database - .HashGetAll( - Arg.Any(), - Arg.Any()) - .Returns( - args => - { - var key = ((RedisKey)args[0]).ToString()[ - (L1L2RedisCacheOptions?.InstanceName?.Length ?? 0)..]; - var value = L2Cache.Get(key); - return - [ - new HashEntry("data", value), - ]; - }); - database - .HashGetAllAsync( - Arg.Any(), - Arg.Any()) - .Returns( - async args => - { - var key = ((RedisKey)args[0]).ToString()[ - (L1L2RedisCacheOptions?.InstanceName?.Length ?? 0)..]; - var value = await L2Cache - .GetAsync(key) - .ConfigureAwait(false); - return - [ - new HashEntry("data", value), - ]; - }); - database - .KeyExists( - Arg.Any(), - Arg.Any()) - .Returns( - args => - { - return L2Cache.Get( - ((RedisKey)args[0]).ToString()) != null; - }); - database - .KeyExistsAsync( - Arg.Any(), - Arg.Any()) - .Returns( - async args => - { - var key = ((RedisKey)args[0]).ToString()[ - (L1L2RedisCacheOptions.InstanceName?.Length ?? 0)..]; - return await L2Cache - .GetAsync(key) - .ConfigureAwait(false) != null; - }); - - var connectionMultiplexer = Substitute - .For(); - connectionMultiplexer - .GetDatabase( - Arg.Any(), - Arg.Any()) - .Returns(database); - - L1L2RedisCacheOptions.ConnectionMultiplexerFactory = - () => Task.FromResult(connectionMultiplexer); - - var messagePublisher = Substitute - .For(); - messagePublisher - .Publish( - Arg.Any(), - Arg.Any()); - messagePublisher - .PublishAsync( - Arg.Any(), - Arg.Any(), - Arg.Any()); - - var messageSubscriber = Substitute - .For(); - messageSubscriber - .SubscribeAsync( - Arg.Any(), - Arg.Any()); - - var messagingConfigurationVerifier = Substitute - .For(); - messagingConfigurationVerifier - .VerifyConfigurationAsync( - Arg.Any(), - Arg.Any()); - - L1L2Cache = new L1L2RedisCache( - L1Cache, - L1L2RedisCacheOptions, - new Func(() => L2Cache), - messagePublisher, - messageSubscriber, - messagingConfigurationVerifier); - } - - public IMemoryCache L1Cache { get; } - public IDistributedCache L1L2Cache { get; } - public L1L2RedisCacheOptions L1L2RedisCacheOptions { get; } - public IDistributedCache L2Cache { get; } - - [TestMethod] - public async Task GetPropagationTest() - { - var key = "key"; - var value = " "u8.ToArray(); - - var prefixedKey = $"{L1L2RedisCacheOptions.KeyPrefix}{key}"; - - await L2Cache - .SetAsync(key, value) - .ConfigureAwait(false); - - Assert.IsNull( - L1Cache.Get(prefixedKey)); - Assert.AreEqual( - value, - await L1L2Cache - .GetAsync(key) - .ConfigureAwait(false)); - Assert.AreEqual( - value, - L1Cache.Get(prefixedKey)); - } - - [TestMethod] - public void SetTest() - { - var key = "key"; - var value = " "u8.ToArray(); - - var prefixedKey = $"{L1L2RedisCacheOptions.KeyPrefix}{key}"; - - L1L2Cache.Set(key, value); - - Assert.AreEqual( - value, - L1L2Cache.Get(key)); - Assert.AreEqual( - value, - L1Cache.Get(prefixedKey)); - Assert.AreEqual( - value, - L2Cache.Get(key)); - } - - [TestMethod] - public void SetRemoveTest() - { - var key = "key"; - var value = " "u8.ToArray(); - - var prefixedKey = $"{L1L2RedisCacheOptions.KeyPrefix}{key}"; - - L1L2Cache.Set(key, value); - - Assert.AreEqual( - value, - L1L2Cache.Get(key)); - Assert.AreEqual( - value, - L1Cache.Get(prefixedKey)); - Assert.AreEqual( - value, - L2Cache.Get(key)); - - L1L2Cache.Remove(key); - - Assert.IsNull( - L1L2Cache.Get(key)); - Assert.IsNull( - L1Cache.Get(prefixedKey)); - Assert.IsNull( - L2Cache.Get(key)); - } - - [TestMethod] - public async Task SetAsyncTest() - { - var key = "key"; - var value = " "u8.ToArray(); - - var prefixedKey = $"{L1L2RedisCacheOptions.KeyPrefix}{key}"; - - await L1L2Cache - .SetAsync(key, value) - .ConfigureAwait(false); - - Assert.AreEqual( - value, - await L1L2Cache - .GetAsync(key) - .ConfigureAwait(false)); - Assert.AreEqual( - value, - L1Cache.Get(prefixedKey)); - Assert.AreEqual( - value, - await L2Cache - .GetAsync(key) - .ConfigureAwait(false)); - } - - [TestMethod] - public async Task SetAsyncRemoveAsyncTest() - { - var key = "key"; - var value = " "u8.ToArray(); - - var prefixedKey = $"{L1L2RedisCacheOptions.KeyPrefix}{key}"; - - await L1L2Cache - .SetAsync(key, value) - .ConfigureAwait(false); - - Assert.AreEqual( - value, - await L1L2Cache - .GetAsync(key) - .ConfigureAwait(false)); - Assert.AreEqual( - value, - L1Cache.Get(prefixedKey)); - Assert.AreEqual( - value, - await L2Cache - .GetAsync(key) - .ConfigureAwait(false)); - - await L1L2Cache - .RemoveAsync(key) - .ConfigureAwait(false); - - Assert.IsNull( - await L1L2Cache - .GetAsync(key) - .ConfigureAwait(false)); - Assert.IsNull( - L1Cache.Get(prefixedKey)); - Assert.IsNull( - await L2Cache - .GetAsync(key) - .ConfigureAwait(false)); - } -} From d17c5c9b7ea5e6b4dd22fc66d79006005b7c2774 Mon Sep 17 00:00:00 2001 From: null-d3v Date: Thu, 12 Dec 2024 11:46:18 -0600 Subject: [PATCH 02/14] fix: use backing ibufferdistributedcache --- labs/MessagingRedisCache/Program.cs | 4 - src/L1L2RedisCache/L1L2RedisCache.cs | 2 + .../ServiceCollectionExtensions.cs | 10 ++ .../MessagingRedisCache.cs | 144 +++++++++++++++--- .../Unit/MessagingRedisCacheTests.cs | 4 +- 5 files changed, 135 insertions(+), 29 deletions(-) diff --git a/labs/MessagingRedisCache/Program.cs b/labs/MessagingRedisCache/Program.cs index 3f87ffd..d82aa5b 100644 --- a/labs/MessagingRedisCache/Program.cs +++ b/labs/MessagingRedisCache/Program.cs @@ -1,9 +1,7 @@ using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; var services = new ServiceCollection(); -services.AddLogging(builder => builder.AddConsole().AddFilter(null, LogLevel.Trace)); services.AddHybridCache(); services.AddMessagingRedisCache(options => { @@ -34,5 +32,3 @@ await hybridCache cancellationToken => ValueTask.FromResult(null)) .ConfigureAwait(false); - -var test = ""; diff --git a/src/L1L2RedisCache/L1L2RedisCache.cs b/src/L1L2RedisCache/L1L2RedisCache.cs index 38fbd34..d67179f 100644 --- a/src/L1L2RedisCache/L1L2RedisCache.cs +++ b/src/L1L2RedisCache/L1L2RedisCache.cs @@ -12,6 +12,7 @@ namespace L1L2RedisCache; /// A distributed cache implementation using both memory and Redis. /// public class L1L2RedisCache( + IBufferDistributedCache bufferDistributedCache, IMemoryCache l1Cache, IMessagePublisher messagePublisher, IMessageSubscriber messageSubscriber, @@ -19,6 +20,7 @@ public class L1L2RedisCache( IOptions messagingRedisCacheOptionsAccessor, ILogger? logger = null) : MessagingRedisCache.MessagingRedisCache( + bufferDistributedCache, messagePublisher, messageSubscriber, messagingConfigurationVerifier, diff --git a/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs b/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs index f07728e..196410c 100644 --- a/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs +++ b/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using MessagingRedisCache; using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using StackExchange.Redis; @@ -46,6 +47,15 @@ public static IMessagingRedisCacheBuilder AddMessagingRedisCache( } } }); + services.AddSingleton( + serviceProvider => + new RedisCache( + serviceProvider + .GetRequiredService>())); + services.AddSingleton( + serviceProvider => new Func( + () => new RedisCache( + serviceProvider.GetRequiredService>()))); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/MessagingRedisCache/MessagingRedisCache.cs b/src/MessagingRedisCache/MessagingRedisCache.cs index eca4460..947d1a6 100644 --- a/src/MessagingRedisCache/MessagingRedisCache.cs +++ b/src/MessagingRedisCache/MessagingRedisCache.cs @@ -1,10 +1,10 @@ -using System.Buffers; -using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StackExchange.Redis; +using System.Buffers; namespace MessagingRedisCache; @@ -12,20 +12,23 @@ namespace MessagingRedisCache; /// A Redis distributed cache implementation that uses pub/sub. /// public class MessagingRedisCache : - RedisCache, - IBufferDistributedCache + IBufferDistributedCache, + IDisposable { /// /// Initializes a new instance of MessagingRedisCache. /// public MessagingRedisCache( + IBufferDistributedCache bufferDistributedCache, IMessagePublisher messagePublisher, IMessageSubscriber messageSubscriber, IMessagingConfigurationVerifier messagingConfigurationVerifier, IOptions messagingRedisCacheOptionsAccessor, - ILogger? logger = null) : - base(messagingRedisCacheOptionsAccessor) + ILogger? logger = null) { + BufferDistributedCache = bufferDistributedCache ?? + throw new ArgumentNullException( + nameof(bufferDistributedCache)); Logger = logger ?? NullLogger.Instance; MessagePublisher = messagePublisher ?? @@ -56,6 +59,11 @@ public MessagingRedisCache( SubscribeCancellationTokenSource.Token); } + /// + /// The backing IBufferDistributedCache, implemented by . + /// + public IBufferDistributedCache BufferDistributedCache { get; } + /// /// The StackExchange.Redis.IDatabase for the . /// @@ -90,29 +98,69 @@ public MessagingRedisCache( private CancellationTokenSource SubscribeCancellationTokenSource { get; set; } /// - public new void Dispose() + public void Dispose() { - base.Dispose(); Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public byte[]? Get(string key) + { + return BufferDistributedCache + .Get(key); + } + + /// + public async Task GetAsync( + string key, + CancellationToken token = default) + { + return await BufferDistributedCache + .GetAsync( + key, + token) + .ConfigureAwait(false); } /// - public new void Remove(string key) + public void Refresh(string key) { - base.Remove(key); + BufferDistributedCache + .Refresh(key); + } + + /// + public async Task RefreshAsync( + string key, + CancellationToken token = default) + { + await BufferDistributedCache + .RefreshAsync( + key, + token) + .ConfigureAwait(false); + } + + /// + public void Remove(string key) + { + BufferDistributedCache + .Remove(key); MessagePublisher.Publish( Database.Value.Multiplexer, key); } /// - public new async Task RemoveAsync( + public async Task RemoveAsync( string key, CancellationToken token = default) { - await base.RemoveAsync( - key, - token) + await BufferDistributedCache + .RemoveAsync( + key, + token) .ConfigureAwait(false); await MessagePublisher .PublishAsync( @@ -123,12 +171,12 @@ await MessagePublisher } /// - public new void Set( + public void Set( string key, byte[] value, DistributedCacheEntryOptions options) { - base.Set( + BufferDistributedCache.Set( key, value, options); @@ -143,24 +191,28 @@ public void Set( ReadOnlySequence value, DistributedCacheEntryOptions options) { - ((IBufferDistributedCache)this).Set(key, value, options); + BufferDistributedCache.Set( + key, + value, + options); MessagePublisher.Publish( Database.Value.Multiplexer, key); } /// - public new async Task SetAsync( + public async Task SetAsync( string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) { - await base.SetAsync( - key, - value, - options, - token) + await BufferDistributedCache + .SetAsync( + key, + value, + options, + token) .ConfigureAwait(false); await MessagePublisher .PublishAsync( @@ -170,7 +222,51 @@ await MessagePublisher .ConfigureAwait(false); } - private void Dispose(bool isDisposing) + public async ValueTask SetAsync( + string key, + ReadOnlySequence value, + DistributedCacheEntryOptions options, + CancellationToken token = default) + { + await BufferDistributedCache + .SetAsync( + key, + value, + options, + token) + .ConfigureAwait(false); + await MessagePublisher + .PublishAsync( + Database.Value.Multiplexer, + key, + token) + .ConfigureAwait(false); + } + + public bool TryGet( + string key, + IBufferWriter destination) + { + return BufferDistributedCache + .TryGet( + key, + destination); + } + + public async ValueTask TryGetAsync( + string key, + IBufferWriter destination, + CancellationToken token = default) + { + return await BufferDistributedCache + .TryGetAsync( + key, + destination, + token) + .ConfigureAwait(false); + } + + protected virtual void Dispose(bool isDisposing) { if (IsDisposed) { diff --git a/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs b/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs index 434f098..26b030a 100644 --- a/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs +++ b/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs @@ -19,7 +19,6 @@ public MessagingRedisCacheTests() var database = Substitute .For(); - ConnectionMultiplexer = Substitute .For(); ConnectionMultiplexer @@ -57,7 +56,10 @@ public MessagingRedisCacheTests() Arg.Any(), Arg.Any()); + var bufferDistributedCache = Substitute + .For(); MessagingRedisCache = new MessagingRedisCache( + bufferDistributedCache, MessagePublisher, MessageSubscriber, messagingConfigurationVerifier, From 01c86edebfd0579169cb9ae92438b2d21540cb7a Mon Sep 17 00:00:00 2001 From: null-d3v Date: Thu, 12 Dec 2024 11:59:51 -0600 Subject: [PATCH 03/14] chore: readme --- README.md | 42 +++---------------------------- src/L1L2RedisCache/README.md | 4 +-- src/MessagingRedisCache/README.md | 10 ++++---- 3 files changed, 11 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index a16af3e..af90646 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,7 @@ -# L1L2RedisCache - -`L1L2RedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) with a strong focus on performance. It leverages [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a level 2 cache, with level 1 evictions being managed via [Redis pub/sub](https://redis.io/topics/pubsub). - -`L1L2RedisCache` is heavily inspired by development insights provided over the past several years by [StackOverflow](https://stackoverflow.com/). It attempts to simplify those concepts into a highly accessible `IDistributedCache` implementation that is more performant. - -I expect to gracefully decomission this project when [`StackExchange.Redis`](https://github.com/StackExchange/StackExchange.Redis) has [client-side caching](https://redis.io/docs/latest/develop/use/client-side-caching/) support. - -## Configuration - -It is intended that L1L12RedisCache be used as an `IDistributedCache` implementation. - -`L1L2RedisCache` can be registered during startup with the following `IServiceCollection` extension method: +# MessagingRedisCache -``` -services.AddL1L2RedisCache(options => -{ - options.Configuration = "localhost"; - options.InstanceName = "Namespace:Prefix:"; -}); -``` +[`MessagingRedisCache`](/src/MessagingRedisCache/README.md) is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) using [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a base implementation. `MessagingRedisCache` will utilize [Redis pub/sub](https://redis.io/topics/pubsub) to ensure that cache entries can be synchronized in a distributed system, where direct deferral to Redis is not always performant. Because of this, it is a functional backing store for [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) which will also evict the `IMemoryCache` entries in distributed systems. -`L1L2RedisCache` options are an extension of the standard `RedisCache` [`RedisCacheOptions`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCacheOptions.cs). The following additional customizations are supported: - -### MessagingType - -The type of messaging system to use for L1 memory cache eviction. - -| MessagingType | Description | Suggestion | -| - | - | - | -| `Default` | Use standard `L1L2RedisCache` [pub/sub](https://redis.io/topics/pubsub) messages for L1 memory cache eviction. | Default behavior. The Redis server requires no additional configuration. | -| `KeyeventNotifications` | Use [keyevent notifications](https://redis.io/topics/notifications) for L1 memory eviction instead of standard `L1L2RedisCache` [pub/sub](https://redis.io/topics/pubsub) messages. The Redis server must have keyevent notifications enabled. | This is only advisable if the Redis server is already using [keyevent notifications](https://redis.io/topics/notifications) with at least a `ghE` configuration and the majority of keys in the server are managed by `L1L2RedisCache`. | -| `KeyspaceNotifications` | Use [keyspace notifications](https://redis.io/topics/notifications) for L1 memory eviction instead of standard `L1L2RedisCache` [pub/sub](https://redis.io/topics/pubsub) messages. The Redis server must have keyspace notifications enabled. | This is only advisable if the Redis server is already using [keyevent notifications](https://redis.io/topics/notifications) with at least a `ghK` configuration and the majority of keys in the server are managed by `L1L2RedisCache`. | - -## Performance - -L1L2RedisCache will generally outperform `RedisCache`, especially in cases of high volume or large cache entries. As entries are opportunistically pulled from memory instead of Redis, costs of latency, network, and Redis operations are avoided. Respective performance gains will rely heavily on the impact of afforementioned factors. - -## Considerations +# L1L2RedisCache -Due to the complex nature of a distributed L1 memory cache, cache entries with sliding expirations are only stored in L2 (Redis). These entries will show no performance improvement over the standard `RedisCache`, but incur no performance penalty. +[`L1L2RedisCache`](/src/L1L2RedisCache/README.MD) is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) with emphasis on performance. It leverages [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`MessagingRedisCache`](/src/MessagingRedisCache/README.md) as a level 2 cache, with level 1 evictions being managed via [Redis pub/sub](https://redis.io/topics/pubsub). \ No newline at end of file diff --git a/src/L1L2RedisCache/README.md b/src/L1L2RedisCache/README.md index cdc8e77..d29c088 100644 --- a/src/L1L2RedisCache/README.md +++ b/src/L1L2RedisCache/README.md @@ -1,10 +1,10 @@ # L1L2RedisCache -`L1L2RedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) with emphasis on performance. It leverages [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a level 2 cache, with level 1 evictions being managed via [Redis pub/sub](https://redis.io/topics/pubsub) by way of [`MemoryCache`](lol.com). +`L1L2RedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) with emphasis on performance. It leverages [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`MessagingRedisCache`](../MessagingRedisCache/README.md) as a level 2 cache, with level 1 evictions being managed via [Redis pub/sub](https://redis.io/topics/pubsub). `L1L2RedisCache` is heavily inspired by development insights provided over the past several years by [StackOverflow](https://stackoverflow.com/). It attempts to simplify those concepts into a highly accessible `IDistributedCache` implementation that is more performant. -I expect to gracefully decomission this project when [`StackExchange.Redis`](https://github.com/StackExchange/StackExchange.Redis) has [client-side caching](https://redis.io/docs/latest/develop/use/client-side-caching/) support. +Using a [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) combined with [`MessagingRedisCache`](../MessagingRedisCache/README.md) is a viable alternative to this project. It is possible that with the full release of `HybridCache` this project will no longer be necessary. ## Configuration diff --git a/src/MessagingRedisCache/README.md b/src/MessagingRedisCache/README.md index 6db442a..b19e9b1 100644 --- a/src/MessagingRedisCache/README.md +++ b/src/MessagingRedisCache/README.md @@ -1,14 +1,14 @@ # MessagingRedisCache -`MessagingRedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) based on [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs). It will utilize [Redis pub/sub](https://redis.io/topics/pubsub) to ensure that cache entries can be synchronized in a distributed system, where direct deferral to Redis is not always performant. Because of this, it is a valuable backing store for [HybridCache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid), and is used as the `IDistributedCache` for [`L1L2RedisCache`](lol.com). +`MessagingRedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) using [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a base implementation. `MessagingRedisCache` will utilize [Redis pub/sub](https://redis.io/topics/pubsub) to ensure that cache entries can be synchronized in a distributed system, where direct deferral to Redis is not always performant. Because of this, it is a functional backing store for [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) which will also evict the `IMemoryCache` entries in distributed systems. -All changes to entries in `MessagingRedisCache` by way of removal or updates, will result in a message being published. The default implementation is to innately publish these messages. Alternatively, [keyevent notifications](https://redis.io/topics/notifications) or [keyspace notifications](https://redis.io/topics/notifications) can be used if the Redis server is configured for them. +All changes to entries in `MessagingRedisCache` by way of removal or updates, will result in a Redis message being published. The default implementation is to directly publish these messages in an established channel. Alternatively, [keyevent notifications](https://redis.io/topics/notifications) or [keyspace notifications](https://redis.io/topics/notifications) can be used if the Redis server is configured for them. I expect to gracefully decomission this project when [`StackExchange.Redis`](https://github.com/StackExchange/StackExchange.Redis) has [client-side caching](https://redis.io/docs/latest/develop/use/client-side-caching/) support. ## Configuration -It is intended that `MessagingRedisCache` be used in conjunction with another kind of layered caching solution, specificially [HybridCache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) or [`L1L2RedisCache`](lol.com). Without any kind of layered cache solution, `MessagingRedisCache` will only publish Redis pub/sub messages for another consumer. +It is intended that `MessagingRedisCache` be used in conjunction with another kind of layered caching solution, specificially [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) or [`L1L2RedisCache`](../L1L2RedisCache/README.md). Without any kind of layered cache solution, `MessagingRedisCache` will only publish Redis pub/sub messages for another consumer. `MessagingRedisCache` can be registered during startup with the following `IServiceCollection` extension method: @@ -35,11 +35,11 @@ services .AddMemoryCacheSubscriber(); ``` -This should be done explicitly when using [HybridCache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) and is done automatically when using [`L1L2RedisCache`](lol.com). +This should be done explicitly when using [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) and is done automatically when using [`L1L2RedisCache`](../L1L2RedisCache/README.md). ## MessagingRedisCacheOptions -`MessagingRedisCacheOptions` are an extension of the standard `RedisCache` [`RedisCacheOptions`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCacheOptions.cs). The following additional customizations are supported: +`MessagingRedisCacheOptions` are an extension of the standard [`RedisCacheOptions`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCacheOptions.cs). The following additional customizations are supported: ### MessagingType From 6d027b22f74da1da8d540785821666ef45f83d89 Mon Sep 17 00:00:00 2001 From: null-d3v Date: Thu, 12 Dec 2024 12:11:10 -0600 Subject: [PATCH 04/14] chore: readme --- README.md | 4 ++-- src/L1L2RedisCache/README.md | 10 ++++++---- src/MessagingRedisCache/README.md | 8 ++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index af90646..1b47fc8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MessagingRedisCache -[`MessagingRedisCache`](/src/MessagingRedisCache/README.md) is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) using [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a base implementation. `MessagingRedisCache` will utilize [Redis pub/sub](https://redis.io/topics/pubsub) to ensure that cache entries can be synchronized in a distributed system, where direct deferral to Redis is not always performant. Because of this, it is a functional backing store for [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) which will also evict the `IMemoryCache` entries in distributed systems. +[`MessagingRedisCache`](/src/MessagingRedisCache/README.md) is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) using [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a base implementation. `MessagingRedisCache` will utilize [Redis pub/sub](https://redis.io/topics/pubsub) to ensure that memory cache entries can be synchronized in a distributed system. This makes it a viable backing store for [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid), where it can evict `IMemoryCache` entries in distributed systems. # L1L2RedisCache -[`L1L2RedisCache`](/src/L1L2RedisCache/README.MD) is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) with emphasis on performance. It leverages [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`MessagingRedisCache`](/src/MessagingRedisCache/README.md) as a level 2 cache, with level 1 evictions being managed via [Redis pub/sub](https://redis.io/topics/pubsub). \ No newline at end of file +[`L1L2RedisCache`](/src/L1L2RedisCache/README.MD) is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs), leveraging [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`MessagingRedisCache`](/src/MessagingRedisCache/README.md) as a level 2 cache. Level 1 evictions are managed via `MessagingRedisCache`'s [Redis pub/sub](https://redis.io/topics/pubsub). \ No newline at end of file diff --git a/src/L1L2RedisCache/README.md b/src/L1L2RedisCache/README.md index d29c088..50e4020 100644 --- a/src/L1L2RedisCache/README.md +++ b/src/L1L2RedisCache/README.md @@ -1,10 +1,10 @@ # L1L2RedisCache -`L1L2RedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) with emphasis on performance. It leverages [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`MessagingRedisCache`](../MessagingRedisCache/README.md) as a level 2 cache, with level 1 evictions being managed via [Redis pub/sub](https://redis.io/topics/pubsub). +`L1L2RedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs), leveraging [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`MessagingRedisCache`](../MessagingRedisCache/README.md) as a level 2 cache. Level 1 evictions are managed via `MessagingRedisCache`'s [Redis pub/sub](https://redis.io/topics/pubsub). -`L1L2RedisCache` is heavily inspired by development insights provided over the past several years by [StackOverflow](https://stackoverflow.com/). It attempts to simplify those concepts into a highly accessible `IDistributedCache` implementation that is more performant. +`L1L2RedisCache` is heavily inspired by development insights provided over the past several years by [StackOverflow](https://stackoverflow.com/). It attempts to simplify those concepts into a highly accessible `IDistributedCache` implementation that is more performant than a baseline `RedisCache`. -Using a [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) combined with [`MessagingRedisCache`](../MessagingRedisCache/README.md) is a viable alternative to this project. It is possible that with the full release of `HybridCache` this project will no longer be necessary. +Using a [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) combined with [`MessagingRedisCache`](../MessagingRedisCache/README.md) is a viable alternative to this project. It is possible that with increased functionality of `HybridCache` that this project will no longer be necessary. ## Configuration @@ -20,6 +20,8 @@ services.AddL1L2RedisCache(options => }); ``` +## MessagingRedisCacheOptions + `L1L2RedisCache` uses `MessagingRedisCacheOptions` which extend the standard `RedisCache` [`RedisCacheOptions`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCacheOptions.cs). The following additional customizations are supported: ### MessagingType @@ -34,7 +36,7 @@ The type of messaging system to use for L1 memory cache eviction. ## Performance -L1L2RedisCache will generally outperform `RedisCache`, especially in cases of high volume or large cache entries. As entries are opportunistically pulled from memory instead of Redis, costs of latency, network, and Redis operations are avoided. Respective performance gains will rely heavily on the impact of afforementioned factors. +L1L2RedisCache will generally outperform a baseline `RedisCache`, particularly in cases of high volume or large cache entries. As entries are opportunistically pulled from memory instead of Redis, where costs of latency, network, and Redis operations are avoided. Respective performance gains will rely heavily on the impact of the afforementioned factors. ## Considerations diff --git a/src/MessagingRedisCache/README.md b/src/MessagingRedisCache/README.md index b19e9b1..1373d51 100644 --- a/src/MessagingRedisCache/README.md +++ b/src/MessagingRedisCache/README.md @@ -1,14 +1,14 @@ # MessagingRedisCache -`MessagingRedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) using [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a base implementation. `MessagingRedisCache` will utilize [Redis pub/sub](https://redis.io/topics/pubsub) to ensure that cache entries can be synchronized in a distributed system, where direct deferral to Redis is not always performant. Because of this, it is a functional backing store for [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) which will also evict the `IMemoryCache` entries in distributed systems. +`MessagingRedisCache` is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs) using [`RedisCache`](https://github.com/dotnet/aspnetcore/blob/main/src/Caching/StackExchangeRedis/src/RedisCache.cs) as a base implementation. `MessagingRedisCache` will utilize [Redis pub/sub](https://redis.io/topics/pubsub) to ensure that memory cache entries can be synchronized in a distributed system. This makes it a viable backing store for [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid), where it can evict `IMemoryCache` entries in distributed systems. -All changes to entries in `MessagingRedisCache` by way of removal or updates, will result in a Redis message being published. The default implementation is to directly publish these messages in an established channel. Alternatively, [keyevent notifications](https://redis.io/topics/notifications) or [keyspace notifications](https://redis.io/topics/notifications) can be used if the Redis server is configured for them. +All changes to entries in `MessagingRedisCache` by way of removal or updates will result in a Redis message being published. The default implementation is to directly publish these messages in an established channel. Alternatively, [keyevent notifications](https://redis.io/topics/notifications) or [keyspace notifications](https://redis.io/topics/notifications) can be used if the Redis server is configured for them. -I expect to gracefully decomission this project when [`StackExchange.Redis`](https://github.com/StackExchange/StackExchange.Redis) has [client-side caching](https://redis.io/docs/latest/develop/use/client-side-caching/) support. +I expect to gracefully decomission this project when [`StackExchange.Redis`](https://github.com/StackExchange/StackExchange.Redis) has [client-side caching](https://redis.io/docs/latest/develop/use/client-side-caching/) support or if [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) similarly implements client-side eviction. ## Configuration -It is intended that `MessagingRedisCache` be used in conjunction with another kind of layered caching solution, specificially [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) or [`L1L2RedisCache`](../L1L2RedisCache/README.md). Without any kind of layered cache solution, `MessagingRedisCache` will only publish Redis pub/sub messages for another consumer. +It is intended that `MessagingRedisCache` be used in conjunction with another kind of layered caching solution, specificially [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) or [`L1L2RedisCache`](../L1L2RedisCache/README.md). Without any kind of layered cache solution, `MessagingRedisCache` will only publish Redis pub/sub messages for another consumer unconfigured by this project. `MessagingRedisCache` can be registered during startup with the following `IServiceCollection` extension method: From 783b159e7e8e043f35c6245a3ff0104def2b1c88 Mon Sep 17 00:00:00 2001 From: null-d3v Date: Thu, 12 Dec 2024 12:12:06 -0600 Subject: [PATCH 05/14] chore: readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b47fc8..2cf6879 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ # L1L2RedisCache -[`L1L2RedisCache`](/src/L1L2RedisCache/README.MD) is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs), leveraging [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`MessagingRedisCache`](/src/MessagingRedisCache/README.md) as a level 2 cache. Level 1 evictions are managed via `MessagingRedisCache`'s [Redis pub/sub](https://redis.io/topics/pubsub). \ No newline at end of file +[`L1L2RedisCache`](/src/L1L2RedisCache/README.md) is an implementation of [`IDistributedCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IDistributedCache.cs), leveraging [`IMemoryCache`](https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/IMemoryCache.cs) as a level 1 cache and [`MessagingRedisCache`](/src/MessagingRedisCache/README.md) as a level 2 cache. Level 1 evictions are managed via `MessagingRedisCache`'s [Redis pub/sub](https://redis.io/topics/pubsub). \ No newline at end of file From 117b0d546f464e715ec68f4c87232ecb67fdf831 Mon Sep 17 00:00:00 2001 From: null-d3v Date: Thu, 12 Dec 2024 12:15:10 -0600 Subject: [PATCH 06/14] chore: docs --- src/L1L2RedisCache/L1L2RedisCache.cs | 41 ++++------------------------ 1 file changed, 6 insertions(+), 35 deletions(-) diff --git a/src/L1L2RedisCache/L1L2RedisCache.cs b/src/L1L2RedisCache/L1L2RedisCache.cs index d67179f..807ccb8 100644 --- a/src/L1L2RedisCache/L1L2RedisCache.cs +++ b/src/L1L2RedisCache/L1L2RedisCache.cs @@ -39,11 +39,7 @@ public class L1L2RedisCache( private SemaphoreSlim KeySemaphore { get; } = new SemaphoreSlim(1, 1); - /// - /// Gets a value with the given key. - /// - /// A string identifying the requested value. - /// The located value or null. + /// [SuppressMessage("Reliability", "CA2000")] public new byte[]? Get(string key) { @@ -84,12 +80,7 @@ public class L1L2RedisCache( return value; } - /// - /// Gets a value with the given key. - /// - /// A string identifying the requested value. - /// Optional. The System.Threading.CancellationToken used to propagate notifications that the operation should be canceled. - /// The System.Threading.Tasks.Task that represents the asynchronous operation, containing the located value or null. + /// public new async Task GetAsync( string key, CancellationToken token = default) @@ -138,10 +129,7 @@ await semaphore return value; } - /// - /// Removes the value with the given key. - /// - /// A string identifying the requested value. + /// [SuppressMessage("Reliability", "CA2000")] public new void Remove(string key) { @@ -160,12 +148,7 @@ await semaphore } } - /// - /// Removes the value with the given key. - /// - /// A string identifying the requested value. - /// Optional. The System.Threading.CancellationToken used to propagate notifications that the operation should be canceled. - /// The System.Threading.Tasks.Task that represents the asynchronous operation. + /// public new async Task RemoveAsync( string key, CancellationToken token = default) @@ -193,12 +176,7 @@ await base } } - /// - /// Sets a value with the given key. - /// - /// A string identifying the requested value. - /// The value to set in the cache. - /// The cache options for the value. + /// [SuppressMessage("Reliability", "CA2000")] public new void Set( string key, @@ -221,14 +199,7 @@ await base } } - /// - /// Sets a value with the given key. - /// - /// A string identifying the requested value. - /// The value to set in the cache. - /// The cache options for the value. - /// Optional. The System.Threading.CancellationToken used to propagate notifications that the operation should be canceled. - /// The System.Threading.Tasks.Task that represents the asynchronous operation. + /// public new async Task SetAsync( string key, byte[] value, From 5992b1b2b3d938d847a5da54acf13f00a8a06fba Mon Sep 17 00:00:00 2001 From: null-d3v Date: Thu, 12 Dec 2024 12:16:50 -0600 Subject: [PATCH 07/14] chore: lab --- Directory.Packages.props | 2 -- labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj | 2 -- 2 files changed, 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 42660ac..2436143 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -13,8 +13,6 @@ - - diff --git a/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj b/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj index 87502fd..45e232b 100644 --- a/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj +++ b/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj @@ -13,7 +13,5 @@ - - From 960a4c59f5d70fc6167e25a3e8b485814695cd1b Mon Sep 17 00:00:00 2001 From: null-d3v Date: Thu, 12 Dec 2024 12:17:40 -0600 Subject: [PATCH 08/14] chore: devcontainer --- .devcontainer/devcontainer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d9739aa..321f27c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -20,7 +20,6 @@ "redis:6379" ], "name": "L1L2RedisCache", - "postCreateCommand": "dotnet dev-certs https", "remoteUser": "root", "service": "devcontainer", "shutdownAction": "stopCompose", From 10e8b4940ac12cceb4facca918edceea4d5fad5e Mon Sep 17 00:00:00 2001 From: null-d3v Date: Thu, 12 Dec 2024 12:20:07 -0600 Subject: [PATCH 09/14] chore: docs --- src/MessagingRedisCache/MessagingRedisCache.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/MessagingRedisCache/MessagingRedisCache.cs b/src/MessagingRedisCache/MessagingRedisCache.cs index 947d1a6..1868fa8 100644 --- a/src/MessagingRedisCache/MessagingRedisCache.cs +++ b/src/MessagingRedisCache/MessagingRedisCache.cs @@ -222,6 +222,7 @@ await MessagePublisher .ConfigureAwait(false); } + /// public async ValueTask SetAsync( string key, ReadOnlySequence value, @@ -243,6 +244,7 @@ await MessagePublisher .ConfigureAwait(false); } + /// public bool TryGet( string key, IBufferWriter destination) @@ -253,6 +255,7 @@ public bool TryGet( destination); } + /// public async ValueTask TryGetAsync( string key, IBufferWriter destination, From 800d718ded5ad6c095df935cf24b296a560b3aa0 Mon Sep 17 00:00:00 2001 From: null-d3v Date: Thu, 12 Dec 2024 12:21:00 -0600 Subject: [PATCH 10/14] chore: tests --- tests/MessagingRedisCache/System/HybridCacheTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/MessagingRedisCache/System/HybridCacheTests.cs b/tests/MessagingRedisCache/System/HybridCacheTests.cs index 2b36dc2..a5ce67b 100644 --- a/tests/MessagingRedisCache/System/HybridCacheTests.cs +++ b/tests/MessagingRedisCache/System/HybridCacheTests.cs @@ -37,8 +37,8 @@ public override void TestInitialize() [TestMethod] public async Task GetOrCreateAsyncTest() { - // await SetAndVerifyConfigurationAsync() - // .ConfigureAwait(false); + await SetAndVerifyConfigurationAsync() + .ConfigureAwait(false); var secondaryMessageSubscriber = SecondaryServiceProvider .GetRequiredService(); From c01a3eaac18a0ee51ea4ea2474e9475c83d99170 Mon Sep 17 00:00:00 2001 From: null-d3v Date: Wed, 26 Nov 2025 14:25:35 -0600 Subject: [PATCH 11/14] chore: net10 --- .devcontainer/docker-compose.yml | 2 +- .editorconfig | 3 + .gitattributes | 1 + .github/workflows/publish.yml | 2 +- .vscode/launch.json | 2 +- Directory.Packages.props | 32 +++-- global.json | 6 + .../MessagingRedisCache.Lab.csproj | 2 +- .../ServiceCollectionExtensions.cs | 31 +++-- .../HashEntryArrayExtensions.cs | 56 ++++---- src/L1L2RedisCache/L1L2RedisCache.csproj | 2 +- .../MessagingRedisCacheBuilderExtensions.cs | 51 ++++---- .../ServiceCollectionExtensions.cs | 123 +++++++++--------- .../MessagingRedisCache.csproj | 2 +- .../MessagingRedisCacheOptions.cs | 4 +- .../System/L1L2RedisCache.Tests.System.csproj | 17 ++- tests/L1L2RedisCache/System/MessagingTests.cs | 43 +++--- .../L1L2RedisCache/System/PerformanceTests.cs | 21 +-- .../L1L2RedisCache/System/ReliabilityTests.cs | 23 ++-- .../System/DefaultMessagingTests.cs | 6 +- .../System/HybridCacheTests.cs | 20 ++- .../System/KeyeventMessagingTests.cs | 6 +- .../System/KeyspaceMessagingTests.cs | 6 +- .../MessagingRedisCache.Tests.System.csproj | 19 ++- .../System/MessagingTestsBase.cs | 22 +++- tests/MessagingRedisCache/System/TestsBase.cs | 9 +- .../MessagingRedisCache.Tests.Unit.csproj | 17 ++- .../Unit/MessagingRedisCacheTests.cs | 21 ++- 28 files changed, 329 insertions(+), 220 deletions(-) create mode 100644 .gitattributes create mode 100644 global.json diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 22ed388..c2a0419 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,6 +1,6 @@ services: devcontainer: - image: mcr.microsoft.com/devcontainers/dotnet:9.0-bookworm + image: mcr.microsoft.com/devcontainers/dotnet:dev-10.0 volumes: - ..:/workspace:cached command: sleep infinity diff --git a/.editorconfig b/.editorconfig index 0a85bc9..0498167 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,6 +18,9 @@ dotnet_diagnostic.IDE0301.severity = warning dotnet_diagnostic.IDE0303.severity = warning dotnet_diagnostic.IDE0305.severity = warning +dotnet_diagnostic.MSTEST0016.severity = warning +dotnet_diagnostic.MSTEST0049.severity = warning + csharp_space_between_square_brackets = true csharp_style_var_elsewhere = true:warning csharp_style_var_for_built_in_types = true:warning diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2125666 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 900fc41..495ec7e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,7 +24,7 @@ jobs: - name: Setup uses: actions/setup-dotnet@v4 with: - dotnet-version: 9 + dotnet-version: 10 - name: Pack run: dotnet pack -c Release - name: Publish diff --git a/.vscode/launch.json b/.vscode/launch.json index 55292cc..3736cc0 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/labs/MessagingRedisCache/bin/Debug/net9.0/MessagingRedisCache.Lab.dll", + "program": "${workspaceFolder}/labs/MessagingRedisCache/bin/Debug/net10.0/MessagingRedisCache.Lab.dll", "args": [], "cwd": "${workspaceFolder}/labs/MessagingRedisCache", "stopAtEntry": false, diff --git a/Directory.Packages.props b/Directory.Packages.props index 2436143..40e4b31 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,19 +4,25 @@ true - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/global.json b/global.json new file mode 100644 index 0000000..7facb0a --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "test": + { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file diff --git a/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj b/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj index 45e232b..d194bf6 100644 --- a/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj +++ b/labs/MessagingRedisCache/MessagingRedisCache.Lab.csproj @@ -2,7 +2,7 @@ EXTEXP0018; Exe - net9.0 + net10.0 diff --git a/src/L1L2RedisCache/Configuration/ServiceCollectionExtensions.cs b/src/L1L2RedisCache/Configuration/ServiceCollectionExtensions.cs index 751d3aa..5664eb7 100644 --- a/src/L1L2RedisCache/Configuration/ServiceCollectionExtensions.cs +++ b/src/L1L2RedisCache/Configuration/ServiceCollectionExtensions.cs @@ -8,22 +8,25 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceCollectionExtensions { - /// - /// Adds L1L2RedisCache distributed caching services to the specified IServiceCollection. - /// - /// The IServiceCollection so that additional calls can be chained. - public static IServiceCollection AddL1L2RedisCache( - this IServiceCollection services, - Action setupAction) + extension( + IServiceCollection services) { - ArgumentNullException.ThrowIfNull(setupAction); + /// + /// Adds L1L2RedisCache distributed caching services to the specified IServiceCollection. + /// + /// The IServiceCollection so that additional calls can be chained. + public IServiceCollection AddL1L2RedisCache( + Action setupAction) + { + ArgumentNullException.ThrowIfNull(setupAction); - services - .AddMessagingRedisCache(setupAction) - .AddMemoryCacheSubscriber(); - services.AddSingleton(); - services.AddMemoryCache(); + services + .AddMessagingRedisCache(setupAction) + .AddMemoryCacheSubscriber(); + services.AddSingleton(); + services.AddMemoryCache(); - return services; + return services; + } } } \ No newline at end of file diff --git a/src/L1L2RedisCache/HashEntryArrayExtensions.cs b/src/L1L2RedisCache/HashEntryArrayExtensions.cs index 15a842d..ca087c6 100644 --- a/src/L1L2RedisCache/HashEntryArrayExtensions.cs +++ b/src/L1L2RedisCache/HashEntryArrayExtensions.cs @@ -10,38 +10,40 @@ internal static class HashEntryArrayExtensions private const long NotPresent = -1; private const string SlidingExpirationKey = "sldexp"; - internal static DistributedCacheEntryOptions GetDistributedCacheEntryOptions( - this HashEntry[] hashEntries) + extension( + HashEntry[] hashEntries) { - var distributedCacheEntryOptions = new DistributedCacheEntryOptions(); - - var absoluteExpirationHashEntry = hashEntries.FirstOrDefault( - hashEntry => hashEntry.Name == AbsoluteExpirationKey); - if (absoluteExpirationHashEntry.Value.HasValue && - absoluteExpirationHashEntry.Value != NotPresent) + internal DistributedCacheEntryOptions GetDistributedCacheEntryOptions() { - distributedCacheEntryOptions.AbsoluteExpiration = new DateTimeOffset( - (long)absoluteExpirationHashEntry.Value, TimeSpan.Zero); + var distributedCacheEntryOptions = new DistributedCacheEntryOptions(); + + var absoluteExpirationHashEntry = hashEntries.FirstOrDefault( + hashEntry => hashEntry.Name == AbsoluteExpirationKey); + if (absoluteExpirationHashEntry.Value.HasValue && + absoluteExpirationHashEntry.Value != NotPresent) + { + distributedCacheEntryOptions.AbsoluteExpiration = new DateTimeOffset( + (long)absoluteExpirationHashEntry.Value, TimeSpan.Zero); + } + + var slidingExpirationHashEntry = hashEntries.FirstOrDefault( + hashEntry => hashEntry.Name == SlidingExpirationKey); + if (slidingExpirationHashEntry.Value.HasValue && + slidingExpirationHashEntry.Value != NotPresent) + { + distributedCacheEntryOptions.SlidingExpiration = new TimeSpan( + (long)slidingExpirationHashEntry.Value); + } + + return distributedCacheEntryOptions; } - var slidingExpirationHashEntry = hashEntries.FirstOrDefault( - hashEntry => hashEntry.Name == SlidingExpirationKey); - if (slidingExpirationHashEntry.Value.HasValue && - slidingExpirationHashEntry.Value != NotPresent) + internal RedisValue GetRedisValue() { - distributedCacheEntryOptions.SlidingExpiration = new TimeSpan( - (long)slidingExpirationHashEntry.Value); - } - - return distributedCacheEntryOptions; - } + var dataHashEntry = hashEntries.FirstOrDefault( + hashEntry => hashEntry.Name == DataKey); - internal static RedisValue GetRedisValue( - this HashEntry[] hashEntries) - { - var dataHashEntry = hashEntries.FirstOrDefault( - hashEntry => hashEntry.Name == DataKey); - - return dataHashEntry.Value; + return dataHashEntry.Value; + } } } \ No newline at end of file diff --git a/src/L1L2RedisCache/L1L2RedisCache.csproj b/src/L1L2RedisCache/L1L2RedisCache.csproj index 38b0c04..c5031a0 100644 --- a/src/L1L2RedisCache/L1L2RedisCache.csproj +++ b/src/L1L2RedisCache/L1L2RedisCache.csproj @@ -4,7 +4,7 @@ CA1724; README.md true - net8.0;net9.0 + net8.0;net9.0;net10.0 diff --git a/src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilderExtensions.cs b/src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilderExtensions.cs index 7ea5ddc..acab99a 100644 --- a/src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilderExtensions.cs +++ b/src/MessagingRedisCache/Configuration/MessagingRedisCacheBuilderExtensions.cs @@ -9,33 +9,34 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class MessagingRedisCacheBuilderExtensions { - public static IMessagingRedisCacheBuilder AddMemoryCacheSubscriber( - this IMessagingRedisCacheBuilder builder) + extension( + IMessagingRedisCacheBuilder builder) { - ArgumentNullException.ThrowIfNull(builder); - - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.AddSingleton( - serviceProvider => - { - var options = serviceProvider - .GetRequiredService>() - .Value; - - return options.MessagingType switch + public IMessagingRedisCacheBuilder AddMemoryCacheSubscriber() + { + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.AddSingleton( + serviceProvider => { - MessagingType.Default => - serviceProvider.GetRequiredService(), - MessagingType.KeyeventNotifications => - serviceProvider.GetRequiredService(), - MessagingType.KeyspaceNotifications => - serviceProvider.GetRequiredService(), - _ => throw new NotImplementedException(), - }; - }); + var options = serviceProvider + .GetRequiredService>() + .Value; + + return options.MessagingType switch + { + MessagingType.Default => + serviceProvider.GetRequiredService(), + MessagingType.KeyeventNotifications => + serviceProvider.GetRequiredService(), + MessagingType.KeyspaceNotifications => + serviceProvider.GetRequiredService(), + _ => throw new NotImplementedException(), + }; + }); - return builder; + return builder; + } } } \ No newline at end of file diff --git a/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs b/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs index 196410c..225065d 100644 --- a/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs +++ b/src/MessagingRedisCache/Configuration/ServiceCollectionExtensions.cs @@ -13,74 +13,77 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceCollectionExtensions { - /// - /// Adds messaging Redis caching services to the specified IServiceCollection. - /// - /// A IMessagingRedisCacheBuilder so that additional calls can be chained. - [SuppressMessage("Performance", "CA1849")] - public static IMessagingRedisCacheBuilder AddMessagingRedisCache( - this IServiceCollection services, - Action setupAction) + extension( + IServiceCollection services) { - ArgumentNullException.ThrowIfNull(setupAction); + /// + /// Adds messaging Redis caching services to the specified IServiceCollection. + /// + /// A IMessagingRedisCacheBuilder so that additional calls can be chained. + [SuppressMessage("Performance", "CA1849")] + public IMessagingRedisCacheBuilder AddMessagingRedisCache( + Action setupAction) + { + ArgumentNullException.ThrowIfNull(setupAction); - services.AddOptions(); - services.Configure(setupAction); - services.Configure( - options => - { - if (options.ConnectionMultiplexerFactory == null) + services.AddOptions(); + services.Configure(setupAction); + services.Configure( + options => { - if (options.ConfigurationOptions != null) + if (options.ConnectionMultiplexerFactory == null) { - options.ConnectionMultiplexerFactory = () => - Task.FromResult( - ConnectionMultiplexer.Connect( - options.ConfigurationOptions) as IConnectionMultiplexer); + if (options.ConfigurationOptions != null) + { + options.ConnectionMultiplexerFactory = () => + Task.FromResult( + ConnectionMultiplexer.Connect( + options.ConfigurationOptions) as IConnectionMultiplexer); + } + else if (!string.IsNullOrEmpty(options.Configuration)) + { + options.ConnectionMultiplexerFactory = () => + Task.FromResult( + ConnectionMultiplexer.Connect( + options.Configuration) as IConnectionMultiplexer); + } } - else if (!string.IsNullOrEmpty(options.Configuration)) - { - options.ConnectionMultiplexerFactory = () => - Task.FromResult( - ConnectionMultiplexer.Connect( - options.Configuration) as IConnectionMultiplexer); - } - } - }); - services.AddSingleton( - serviceProvider => - new RedisCache( - serviceProvider - .GetRequiredService>())); - services.AddSingleton( - serviceProvider => new Func( - () => new RedisCache( - serviceProvider.GetRequiredService>()))); - services.TryAddSingleton(); - services.TryAddSingleton(); - - services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton( - serviceProvider => - { - var options = serviceProvider - .GetRequiredService>() - .Value; + }); + services.AddSingleton( + serviceProvider => + new RedisCache( + serviceProvider + .GetRequiredService>())); + services.AddSingleton( + serviceProvider => new Func( + () => new RedisCache( + serviceProvider.GetRequiredService>()))); + services.TryAddSingleton(); + services.TryAddSingleton(); - return options.MessagingType switch + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton( + serviceProvider => { - MessagingType.Default => - serviceProvider.GetRequiredService(), - MessagingType.KeyeventNotifications or MessagingType.KeyspaceNotifications => - serviceProvider.GetRequiredService(), - _ => - throw new NotImplementedException(), - }; - }); + var options = serviceProvider + .GetRequiredService>() + .Value; + + return options.MessagingType switch + { + MessagingType.Default => + serviceProvider.GetRequiredService(), + MessagingType.KeyeventNotifications or MessagingType.KeyspaceNotifications => + serviceProvider.GetRequiredService(), + _ => + throw new NotImplementedException(), + }; + }); - services.TryAddSingleton(); + services.TryAddSingleton(); - return new MessagingRedisCacheBuilder(services); + return new MessagingRedisCacheBuilder(services); + } } } diff --git a/src/MessagingRedisCache/MessagingRedisCache.csproj b/src/MessagingRedisCache/MessagingRedisCache.csproj index 3fe3e21..fab2027 100644 --- a/src/MessagingRedisCache/MessagingRedisCache.csproj +++ b/src/MessagingRedisCache/MessagingRedisCache.csproj @@ -4,7 +4,7 @@ CA1724; README.md true - net8.0;net9.0 + net8.0;net9.0;net10.0 diff --git a/src/MessagingRedisCache/MessagingRedisCacheOptions.cs b/src/MessagingRedisCache/MessagingRedisCacheOptions.cs index f90a250..bc3f8e5 100644 --- a/src/MessagingRedisCache/MessagingRedisCacheOptions.cs +++ b/src/MessagingRedisCache/MessagingRedisCacheOptions.cs @@ -5,8 +5,8 @@ namespace MessagingRedisCache; /// /// Configuration options for L1L2RedisCache. /// -public class MessagingRedisCacheOptions() : - RedisCacheOptions() +public class MessagingRedisCacheOptions : + RedisCacheOptions { /// /// The pub/sub channel name. diff --git a/tests/L1L2RedisCache/System/L1L2RedisCache.Tests.System.csproj b/tests/L1L2RedisCache/System/L1L2RedisCache.Tests.System.csproj index b144620..2331458 100644 --- a/tests/L1L2RedisCache/System/L1L2RedisCache.Tests.System.csproj +++ b/tests/L1L2RedisCache/System/L1L2RedisCache.Tests.System.csproj @@ -1,9 +1,15 @@ + true false + true CA1515; - net9.0 + Exe + net10.0 + + + @@ -12,12 +18,17 @@ - - + + + + + + + diff --git a/tests/L1L2RedisCache/System/MessagingTests.cs b/tests/L1L2RedisCache/System/MessagingTests.cs index a1acb68..fe15ba1 100644 --- a/tests/L1L2RedisCache/System/MessagingTests.cs +++ b/tests/L1L2RedisCache/System/MessagingTests.cs @@ -1,4 +1,3 @@ -using System.Text; using MessagingRedisCache; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; @@ -6,22 +5,22 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Text; namespace L1L2RedisCache.Tests.System; [TestClass] -public class MessagingTests +public class MessagingTests( + TestContext testContext) { - public MessagingTests() - { - Configuration = new ConfigurationBuilder() + public IConfiguration Configuration { get; } = + new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build(); - EventTimeout = TimeSpan.FromSeconds(5); - } - - public IConfiguration Configuration { get; } - public TimeSpan EventTimeout { get; } + public TimeSpan EventTimeout { get; } = + TimeSpan.FromSeconds(5); + public TestContext TestContext { get; set; } = + testContext; [DataRow(100, MessagingType.Default)] [DataRow(100, MessagingType.KeyeventNotifications)] @@ -92,7 +91,9 @@ await SetAndVerifyConfigurationAsync( // L1 population via L2 await primaryL1L2Cache .SetStringAsync( - key, value) + key, + value, + token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( messageAutoResetEvent @@ -103,7 +104,9 @@ await primaryL1L2Cache Assert.AreEqual( value, await secondaryL1L2Cache - .GetStringAsync(key) + .GetStringAsync( + key, + token: TestContext.CancellationToken) .ConfigureAwait(false)); Assert.IsTrue( Encoding.ASCII.GetBytes(value).SequenceEqual( @@ -113,7 +116,9 @@ await secondaryL1L2Cache // L1 population via L2 await primaryL1L2Cache .SetStringAsync( - key, value) + key, + value, + token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( messageAutoResetEvent @@ -124,7 +129,9 @@ await primaryL1L2Cache Assert.AreEqual( value, await secondaryL1L2Cache - .GetStringAsync(key) + .GetStringAsync( + key, + token: TestContext.CancellationToken) .ConfigureAwait(false)); Assert.IsTrue( Encoding.ASCII.GetBytes(value).SequenceEqual( @@ -132,7 +139,9 @@ await secondaryL1L2Cache // L1 eviction via remove await primaryL1L2Cache - .RemoveAsync(key) + .RemoveAsync( + key, + token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( messageAutoResetEvent @@ -142,7 +151,9 @@ await primaryL1L2Cache .Get(key)); Assert.IsNull( await secondaryL1L2Cache - .GetStringAsync(key) + .GetStringAsync( + key, + token: TestContext.CancellationToken) .ConfigureAwait(false)); } } diff --git a/tests/L1L2RedisCache/System/PerformanceTests.cs b/tests/L1L2RedisCache/System/PerformanceTests.cs index eb221be..5becbf1 100644 --- a/tests/L1L2RedisCache/System/PerformanceTests.cs +++ b/tests/L1L2RedisCache/System/PerformanceTests.cs @@ -37,16 +37,18 @@ public void GetPerformanceTest() var l2GetVsL1L2GetRatio = l2GetReport.ResultStatistics?.Median / - l1L2GetReport.ResultStatistics?.Median; - Assert.IsTrue( - l2GetVsL1L2GetRatio > 100, + l1L2GetReport.ResultStatistics?.Median ?? 0; + Assert.IsGreaterThan( + 100, + l2GetVsL1L2GetRatio, $"L1L2RedisCache Get must perform significantly better (> 100) than RedisCache Get: {l2GetVsL1L2GetRatio}"); var l1L2GetPropagationVsl2GetRatio = l1L2GetPropagationReport.ResultStatistics?.Median / - l2GetReport.ResultStatistics?.Median; - Assert.IsTrue( - l1L2GetPropagationVsl2GetRatio < 3, + l2GetReport.ResultStatistics?.Median ?? 0; + Assert.IsLessThan( + 3, + l1L2GetPropagationVsl2GetRatio, $"L1L2RedisCache GetPropagation cannot perform significantly worse (< 3) than RedisCache Get: {l1L2GetPropagationVsl2GetRatio}"); } @@ -69,9 +71,10 @@ public void SetPerformanceTest() var l1L2SetVsl2SetRatio = l1L2SetReport.ResultStatistics?.Median / - l2SetReport.ResultStatistics?.Median; - Assert.IsTrue( - l1L2SetVsl2SetRatio < 3, + l2SetReport.ResultStatistics?.Median ?? 0; + Assert.IsLessThan( + 3, + l1L2SetVsl2SetRatio, $"L1L2RedisCache Set cannot perform significantly worse (< 3) than RedisCache Set: {l1L2SetVsl2SetRatio}"); } } \ No newline at end of file diff --git a/tests/L1L2RedisCache/System/ReliabilityTests.cs b/tests/L1L2RedisCache/System/ReliabilityTests.cs index fb254ee..912029c 100644 --- a/tests/L1L2RedisCache/System/ReliabilityTests.cs +++ b/tests/L1L2RedisCache/System/ReliabilityTests.cs @@ -8,18 +8,17 @@ namespace L1L2RedisCache.Tests.System; [TestClass] -public class ReliabilityTests +public class ReliabilityTests( + TestContext testContext) { - public ReliabilityTests() - { - Configuration = new ConfigurationBuilder() + public IConfiguration Configuration { get; } = + new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build(); - EventTimeout = TimeSpan.FromSeconds(5); - } - - public IConfiguration Configuration { get; } - public TimeSpan EventTimeout { get; } + public TimeSpan EventTimeout { get; } = + TimeSpan.FromSeconds(5); + public TestContext TestContext { get; set; } = + testContext; [TestMethod] public void InitializeBadConnectionTest() @@ -48,8 +47,10 @@ public void InitializeBadConnectionTest() Assert.IsFalse( subscribeAutoResetEvent .WaitOne(EventTimeout)); - Assert.ThrowsExceptionAsync( + Assert.ThrowsAsync( () => l1L2Cache - .GetStringAsync(string.Empty)); + .GetStringAsync( + string.Empty, + token: TestContext.CancellationToken)); } } \ No newline at end of file diff --git a/tests/MessagingRedisCache/System/DefaultMessagingTests.cs b/tests/MessagingRedisCache/System/DefaultMessagingTests.cs index 6afce20..b705f07 100644 --- a/tests/MessagingRedisCache/System/DefaultMessagingTests.cs +++ b/tests/MessagingRedisCache/System/DefaultMessagingTests.cs @@ -3,9 +3,11 @@ namespace MessagingRedisCache.Tests.System; [TestClass] -public class DefaultMessagingTests() : +public class DefaultMessagingTests( + TestContext testContext) : MessagingTestsBase( - MessagingType.Default) + MessagingType.Default, + testContext) { [TestInitialize] public override void TestInitialize() diff --git a/tests/MessagingRedisCache/System/HybridCacheTests.cs b/tests/MessagingRedisCache/System/HybridCacheTests.cs index a5ce67b..9250f9d 100644 --- a/tests/MessagingRedisCache/System/HybridCacheTests.cs +++ b/tests/MessagingRedisCache/System/HybridCacheTests.cs @@ -9,8 +9,11 @@ namespace MessagingRedisCache.Tests.System; public class HybridCacheTests : TestsBase { - public HybridCacheTests() : - base(MessagingType.Default) + public HybridCacheTests( + TestContext testContext) : + base( + MessagingType.Default, + testContext) { PrimaryServices.AddHybridCache(); SecondaryServices.AddHybridCache(); @@ -57,9 +60,9 @@ await PrimaryHybridCache .SetAsync( key, value, - HybridCacheEntryOptions) + HybridCacheEntryOptions, + cancellationToken: TestContext.CancellationToken) .ConfigureAwait(false); - var test = await PrimaryDistributedCache.GetAsync(key).ConfigureAwait(false); Assert.IsTrue( messageAutoResetEvent .WaitOne(EventTimeout)); @@ -70,7 +73,8 @@ await PrimaryHybridCache key, cancellationToken => ValueTask.FromResult(null), - HybridCacheEntryOptions) + HybridCacheEntryOptions, + cancellationToken: TestContext.CancellationToken) .ConfigureAwait(false)); Assert.IsNull( SecondaryMemoryCache @@ -82,7 +86,8 @@ await SecondaryHybridCache key, cancellationToken => ValueTask.FromResult(null), - HybridCacheEntryOptions) + HybridCacheEntryOptions, + cancellationToken: TestContext.CancellationToken) .ConfigureAwait(false)); Assert.IsNotNull( SecondaryMemoryCache @@ -90,7 +95,8 @@ await SecondaryHybridCache await PrimaryHybridCache .RemoveAsync( - key) + key, + cancellationToken: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( messageAutoResetEvent diff --git a/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs b/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs index af326d3..54448fc 100644 --- a/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs +++ b/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs @@ -3,9 +3,11 @@ namespace MessagingRedisCache.Tests.System; [TestClass] -public class KeyeventMessagingTests() : +public class KeyeventMessagingTests( + TestContext testContext) : MessagingTestsBase( - MessagingType.KeyeventNotifications) + MessagingType.KeyeventNotifications, + testContext) { [TestInitialize] public override void TestInitialize() diff --git a/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs b/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs index 7e1ea98..f67d7e9 100644 --- a/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs +++ b/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs @@ -3,9 +3,11 @@ namespace MessagingRedisCache.Tests.System; [TestClass] -public class KeyspaceMessagingTests() : +public class KeyspaceMessagingTests( + TestContext testContext) : MessagingTestsBase( - MessagingType.KeyspaceNotifications) + MessagingType.KeyspaceNotifications, + testContext) { [TestInitialize] public override void TestInitialize() diff --git a/tests/MessagingRedisCache/System/MessagingRedisCache.Tests.System.csproj b/tests/MessagingRedisCache/System/MessagingRedisCache.Tests.System.csproj index 6f8e026..4eaf598 100644 --- a/tests/MessagingRedisCache/System/MessagingRedisCache.Tests.System.csproj +++ b/tests/MessagingRedisCache/System/MessagingRedisCache.Tests.System.csproj @@ -1,9 +1,15 @@ + true false - CA1515;EXTEXP0018; - net9.0 + true + CA1515; + Exe + net10.0 + + + @@ -11,13 +17,18 @@ - - + + + + + + + diff --git a/tests/MessagingRedisCache/System/MessagingTestsBase.cs b/tests/MessagingRedisCache/System/MessagingTestsBase.cs index a30c15d..c25b024 100644 --- a/tests/MessagingRedisCache/System/MessagingTestsBase.cs +++ b/tests/MessagingRedisCache/System/MessagingTestsBase.cs @@ -10,9 +10,11 @@ namespace MessagingRedisCache.Tests.System; [TestClass] public abstract class MessagingTestsBase( - MessagingType messagingType) : + MessagingType messagingType, + TestContext testContext) : TestsBase( - messagingType) + messagingType, + testContext) { [TestInitialize] public override void TestInitialize() @@ -40,7 +42,10 @@ await SetAndVerifyConfigurationAsync() var value = Guid.NewGuid().ToString(); await PrimaryDistributedCache - .SetStringAsync(key, value) + .SetStringAsync( + key, + value, + token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( messageAutoResetEvent @@ -50,7 +55,8 @@ await PrimaryDistributedCache .Set(key, value); await PrimaryDistributedCache .RemoveAsync( - key) + key, + token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( messageAutoResetEvent @@ -82,7 +88,10 @@ await SetAndVerifyConfigurationAsync() var value = Guid.NewGuid().ToString(); await PrimaryDistributedCache - .SetStringAsync(key, value) + .SetStringAsync( + key, + value, + token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( messageAutoResetEvent @@ -127,7 +136,8 @@ await PrimaryDistributedCache .SetStringAsync( key, value, - DistributedCacheEntryOptions) + DistributedCacheEntryOptions, + token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( messageAutoResetEvent diff --git a/tests/MessagingRedisCache/System/TestsBase.cs b/tests/MessagingRedisCache/System/TestsBase.cs index 5430b5f..8271d14 100644 --- a/tests/MessagingRedisCache/System/TestsBase.cs +++ b/tests/MessagingRedisCache/System/TestsBase.cs @@ -6,13 +6,14 @@ namespace MessagingRedisCache.Tests.System; -[TestClass] public abstract class TestsBase { internal TestsBase( - MessagingType messagingType) + MessagingType messagingType, + TestContext testContext) { MessagingType = messagingType; + TestContext = testContext; PrimaryServices = new ServiceCollection(); PrimaryServices.AddSingleton(Configuration); @@ -57,6 +58,7 @@ internal TestsBase( public IDistributedCache SecondaryDistributedCache { get; private set; } = default!; public IMemoryCache SecondaryMemoryCache { get; private set; } = default!; public IServiceProvider SecondaryServiceProvider { get; private set; } = default!; + public TestContext TestContext { get; } protected IServiceCollection PrimaryServices { get; private set; } = default!; protected IServiceCollection SecondaryServices { get; private set; } = default!; @@ -98,7 +100,8 @@ await messagingRedisCache!.Database.Value Assert.IsTrue( await configurationVerifier .VerifyConfigurationAsync( - messagingRedisCache.Database.Value) + messagingRedisCache.Database.Value, + cancellationToken: TestContext.CancellationToken) .ConfigureAwait(false)); } } diff --git a/tests/MessagingRedisCache/Unit/MessagingRedisCache.Tests.Unit.csproj b/tests/MessagingRedisCache/Unit/MessagingRedisCache.Tests.Unit.csproj index 27a0fcd..baa77f2 100644 --- a/tests/MessagingRedisCache/Unit/MessagingRedisCache.Tests.Unit.csproj +++ b/tests/MessagingRedisCache/Unit/MessagingRedisCache.Tests.Unit.csproj @@ -1,15 +1,26 @@ + true false + true CA1515; - net9.0 + Exe + net10.0 + + + - - + + + + + + + diff --git a/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs b/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs index 26b030a..f7cd735 100644 --- a/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs +++ b/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs @@ -10,8 +10,11 @@ namespace MessagingRedisCache.Tests.Unit; [TestClass] public class MessagingRedisCacheTests { - public MessagingRedisCacheTests() + public MessagingRedisCacheTests( + TestContext testContext) { + TestContext = testContext; + MessagingRedisCacheOptions = new MessagingRedisCacheOptions { InstanceName = "L1L2RedisCache:Test:", @@ -71,6 +74,7 @@ public MessagingRedisCacheTests() public IMessageSubscriber MessageSubscriber { get; } public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; } public MessagingRedisCache MessagingRedisCache { get; } + public TestContext TestContext { get; set; } [TestMethod] public async Task RemoveAsyncTest() @@ -78,14 +82,17 @@ public async Task RemoveAsyncTest() var key = "key"; await MessagingRedisCache - .RemoveAsync(key) + .RemoveAsync( + key, + token: TestContext.CancellationToken) .ConfigureAwait(false); await MessagePublisher .Received() .PublishAsync( Arg.Any(), - key) + key, + cancellationToken: TestContext.CancellationToken) .ConfigureAwait(false); } @@ -111,14 +118,18 @@ public async Task SetAsyncTest() var value = " "u8.ToArray(); await MessagingRedisCache - .SetAsync(key, value) + .SetAsync( + key, + value, + token: TestContext.CancellationToken) .ConfigureAwait(false); await MessagePublisher .Received() .PublishAsync( Arg.Any(), - key) + key, + cancellationToken: TestContext.CancellationToken) .ConfigureAwait(false); } From 60d5a7dd11fdc5678dfdcc28d7acdb443a00907a Mon Sep 17 00:00:00 2001 From: null-d3v Date: Fri, 28 Nov 2025 12:31:03 -0600 Subject: [PATCH 12/14] chore: events in options --- .editorconfig | 1 + .github/workflows/build.yml | 2 +- .github/workflows/publish.yml | 2 +- L1L2RedisCache.sln | 119 ------------------ L1L2RedisCache.slnx | 19 +++ .../Messaging/DefaultMessageSubscriber.cs | 33 ++--- .../Messaging/IMessageSubscriber.cs | 10 -- .../Messaging/KeyeventMessageSubscriber.cs | 45 +++---- .../Messaging/KeyspaceMessageSubscriber.cs | 37 +++--- .../Messaging/NopMessageSubscriber.cs | 3 - .../Messaging/OnMessageEventArgs.cs | 19 --- .../MessagingRedisCacheEvents.cs | 21 ++++ .../MessagingRedisCacheOptions.cs | 8 +- tests/L1L2RedisCache/System/MessagingTests.cs | 29 ++--- .../L1L2RedisCache/System/PerformanceTests.cs | 1 - .../L1L2RedisCache/System/ReliabilityTests.cs | 18 ++- .../System/DefaultMessagingTests.cs | 2 - .../System/HybridCacheTests.cs | 24 ++-- .../System/KeyeventMessagingTests.cs | 2 - .../System/KeyspaceMessagingTests.cs | 2 - .../System/MessagingTestsBase.cs | 74 +++++------ tests/MessagingRedisCache/System/TestsBase.cs | 18 ++- .../Unit/MessagingRedisCacheTests.cs | 1 - 23 files changed, 189 insertions(+), 301 deletions(-) delete mode 100644 L1L2RedisCache.sln create mode 100644 L1L2RedisCache.slnx delete mode 100644 src/MessagingRedisCache/Messaging/OnMessageEventArgs.cs create mode 100644 src/MessagingRedisCache/MessagingRedisCacheEvents.cs diff --git a/.editorconfig b/.editorconfig index 0498167..188d3c3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,6 +3,7 @@ dotnet_diagnostic.severity = warning dotnet_diagnostic.CS1591.severity = none dotnet_diagnostic.IDE0001.severity = warning +dotnet_diagnostic.IDE0005.severity = warning dotnet_diagnostic.IDE0007.severity = warning dotnet_diagnostic.IDE0008.severity = none dotnet_diagnostic.IDE0010.severity = none diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 872bec3..8a0df48 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,4 +11,4 @@ jobs: uses: devcontainers/ci@v0.3 with: push: never - runCmd: dotnet test + runCmd: dotnet test --max-parallel-test-modules 1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 495ec7e..64b74db 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: uses: devcontainers/ci@v0.3 with: push: never - runCmd: dotnet test + runCmd: dotnet test --max-parallel-test-modules 1 publish: runs-on: ubuntu-latest steps: diff --git a/L1L2RedisCache.sln b/L1L2RedisCache.sln deleted file mode 100644 index e18e42f..0000000 --- a/L1L2RedisCache.sln +++ /dev/null @@ -1,119 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FEEBBE29-BAE7-4407-95D5-EB1F56F6BDA3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache", "src\L1L2RedisCache\L1L2RedisCache.csproj", "{9C09D590-3186-4E27-8103-66C2D6CAFDF2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagingRedisCache", "src\MessagingRedisCache\MessagingRedisCache.csproj", "{71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{E6F22576-963C-4CE3-A653-FBC7DEA5F10A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.System", "tests\MessagingRedisCache\System\MessagingRedisCache.Tests.System.csproj", "{DF934775-FF05-462D-8514-497F68BB8A45}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.Unit", "tests\MessagingRedisCache\Unit\MessagingRedisCache.Tests.Unit.csproj", "{8791FCF7-078D-44A5-AC59-C7C2CE469D3F}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "L1L2RedisCache", "L1L2RedisCache", "{E4234EE7-DAAE-40F5-A24F-14C9C2B5F9BD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "L1L2RedisCache.Tests.System", "tests\L1L2RedisCache\System\L1L2RedisCache.Tests.System.csproj", "{2BA929A4-78E0-4C29-A676-1F5339C99245}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "labs", "labs", "{3E40FF50-EE26-49F6-81B8-94DE4CC9BDF2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessagingRedisCache.Lab", "labs\MessagingRedisCache\MessagingRedisCache.Lab.csproj", "{961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Debug|Any CPU.Build.0 = Debug|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Debug|x64.ActiveCfg = Debug|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Debug|x64.Build.0 = Debug|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Debug|x86.ActiveCfg = Debug|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Debug|x86.Build.0 = Debug|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Release|Any CPU.ActiveCfg = Release|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Release|Any CPU.Build.0 = Release|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Release|x64.ActiveCfg = Release|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Release|x64.Build.0 = Release|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Release|x86.ActiveCfg = Release|Any CPU - {71A8453B-1A5E-49ED-A9A2-17B7DC9A7407}.Release|x86.Build.0 = Release|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Debug|x64.ActiveCfg = Debug|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Debug|x64.Build.0 = Debug|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Debug|x86.ActiveCfg = Debug|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Debug|x86.Build.0 = Debug|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|Any CPU.Build.0 = Release|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|x64.ActiveCfg = Release|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|x64.Build.0 = Release|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|x86.ActiveCfg = Release|Any CPU - {8791FCF7-078D-44A5-AC59-C7C2CE469D3F}.Release|x86.Build.0 = Release|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x64.ActiveCfg = Debug|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x64.Build.0 = Debug|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x86.ActiveCfg = Debug|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Debug|x86.Build.0 = Debug|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Release|Any CPU.Build.0 = Release|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Release|x64.ActiveCfg = Release|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Release|x64.Build.0 = Release|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Release|x86.ActiveCfg = Release|Any CPU - {DF934775-FF05-462D-8514-497F68BB8A45}.Release|x86.Build.0 = Release|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x64.ActiveCfg = Debug|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x64.Build.0 = Debug|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x86.ActiveCfg = Debug|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Debug|x86.Build.0 = Debug|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|Any CPU.Build.0 = Release|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x64.ActiveCfg = Release|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x64.Build.0 = Release|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x86.ActiveCfg = Release|Any CPU - {9C09D590-3186-4E27-8103-66C2D6CAFDF2}.Release|x86.Build.0 = Release|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x64.ActiveCfg = Debug|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x64.Build.0 = Debug|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x86.ActiveCfg = Debug|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Debug|x86.Build.0 = Debug|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|Any CPU.Build.0 = Release|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x64.ActiveCfg = Release|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x64.Build.0 = Release|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x86.ActiveCfg = Release|Any CPU - {2BA929A4-78E0-4C29-A676-1F5339C99245}.Release|x86.Build.0 = Release|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x64.ActiveCfg = Debug|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x64.Build.0 = Debug|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x86.ActiveCfg = Debug|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Debug|x86.Build.0 = Debug|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|Any CPU.Build.0 = Release|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x64.ActiveCfg = Release|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x64.Build.0 = Release|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x86.ActiveCfg = Release|Any CPU - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {DF934775-FF05-462D-8514-497F68BB8A45} = {E6F22576-963C-4CE3-A653-FBC7DEA5F10A} - {9C09D590-3186-4E27-8103-66C2D6CAFDF2} = {FEEBBE29-BAE7-4407-95D5-EB1F56F6BDA3} - {E4234EE7-DAAE-40F5-A24F-14C9C2B5F9BD} = {E6F22576-963C-4CE3-A653-FBC7DEA5F10A} - {2BA929A4-78E0-4C29-A676-1F5339C99245} = {E4234EE7-DAAE-40F5-A24F-14C9C2B5F9BD} - {961D1C90-AD89-4F6F-BBB4-923D9F7C4F3A} = {3E40FF50-EE26-49F6-81B8-94DE4CC9BDF2} - EndGlobalSection -EndGlobal diff --git a/L1L2RedisCache.slnx b/L1L2RedisCache.slnx new file mode 100644 index 0000000..a2d6194 --- /dev/null +++ b/L1L2RedisCache.slnx @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/MessagingRedisCache/Messaging/DefaultMessageSubscriber.cs b/src/MessagingRedisCache/Messaging/DefaultMessageSubscriber.cs index be7cd7d..8f2d43d 100644 --- a/src/MessagingRedisCache/Messaging/DefaultMessageSubscriber.cs +++ b/src/MessagingRedisCache/Messaging/DefaultMessageSubscriber.cs @@ -14,25 +14,25 @@ internal class DefaultMessageSubscriber( memoryCache; public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; set; } = messagingRedisCacheOptionsAccessor.Value; - public EventHandler? OnMessage { get; set; } - public EventHandler? OnSubscribe { get; set; } public async Task SubscribeAsync( IConnectionMultiplexer connectionMultiplexer, CancellationToken cancellationToken = default) { - await connectionMultiplexer + (await connectionMultiplexer .GetSubscriber() .SubscribeAsync( new RedisChannel( MessagingRedisCacheOptions.Channel, - RedisChannel.PatternMode.Literal), - ProcessMessage) - .ConfigureAwait(false); + RedisChannel.PatternMode.Literal)) + .ConfigureAwait(false)) + .OnMessage(ProcessMessageAsync); - OnSubscribe?.Invoke( - this, - EventArgs.Empty); + await MessagingRedisCacheOptions + .Events + .OnSubscribe + .Invoke() + .ConfigureAwait(false); } public async Task UnsubscribeAsync( @@ -48,13 +48,12 @@ await connectionMultiplexer .ConfigureAwait(false); } - internal void ProcessMessage( - RedisChannel channel, - RedisValue message) + internal async Task ProcessMessageAsync( + ChannelMessage channelMessage) { var cacheMessage = JsonSerializer .Deserialize( - message.ToString(), + channelMessage.Message.ToString(), SourceGenerationContext.Default.CacheMessage); if (cacheMessage != null && cacheMessage.PublisherId != MessagingRedisCacheOptions.Id) @@ -62,9 +61,11 @@ internal void ProcessMessage( MemoryCache.Remove( cacheMessage.Key); - OnMessage?.Invoke( - this, - new OnMessageEventArgs(cacheMessage.Key)); + await MessagingRedisCacheOptions + .Events + .OnMessageRecieved + .Invoke(channelMessage) + .ConfigureAwait(false); } } } diff --git a/src/MessagingRedisCache/Messaging/IMessageSubscriber.cs b/src/MessagingRedisCache/Messaging/IMessageSubscriber.cs index 7166c9f..f4c6ca4 100644 --- a/src/MessagingRedisCache/Messaging/IMessageSubscriber.cs +++ b/src/MessagingRedisCache/Messaging/IMessageSubscriber.cs @@ -7,16 +7,6 @@ namespace MessagingRedisCache; /// public interface IMessageSubscriber { - /// - /// An event that is raised when a message is recieved. - /// - EventHandler? OnMessage { get; set; } - - /// - /// An event that is raised when a subscription is created. - /// - EventHandler? OnSubscribe { get; set; } - /// /// Subscribes to messages indicating cache values have changed. /// diff --git a/src/MessagingRedisCache/Messaging/KeyeventMessageSubscriber.cs b/src/MessagingRedisCache/Messaging/KeyeventMessageSubscriber.cs index 67ca1cb..e511feb 100644 --- a/src/MessagingRedisCache/Messaging/KeyeventMessageSubscriber.cs +++ b/src/MessagingRedisCache/Messaging/KeyeventMessageSubscriber.cs @@ -13,34 +13,34 @@ internal class KeyeventMessageSubscriber( memoryCache; public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; set; } = messagingRedisCacheOptionsAccessor.Value; - public EventHandler? OnMessage { get; set; } - public EventHandler? OnSubscribe { get; set; } public async Task SubscribeAsync( IConnectionMultiplexer connectionMultiplexer, CancellationToken cancellationToken = default) { - await connectionMultiplexer + (await connectionMultiplexer .GetSubscriber() .SubscribeAsync( new RedisChannel( "__keyevent@*__:del", - RedisChannel.PatternMode.Pattern), - ProcessMessage) - .ConfigureAwait(false); + RedisChannel.PatternMode.Pattern)) + .ConfigureAwait(false)) + .OnMessage(ProcessMessageAsync); - await connectionMultiplexer + (await connectionMultiplexer .GetSubscriber() .SubscribeAsync( new RedisChannel( "__keyevent@*__:hset", - RedisChannel.PatternMode.Pattern), - ProcessMessage) - .ConfigureAwait(false); + RedisChannel.PatternMode.Pattern)) + .ConfigureAwait(false)) + .OnMessage(ProcessMessageAsync); - OnSubscribe?.Invoke( - this, - EventArgs.Empty); + await MessagingRedisCacheOptions + .Events + .OnSubscribe + .Invoke() + .ConfigureAwait(false); } public async Task UnsubscribeAsync( @@ -64,23 +64,24 @@ await connectionMultiplexer .ConfigureAwait(false); } - internal void ProcessMessage( - RedisChannel channel, - RedisValue message) + internal async Task ProcessMessageAsync( + ChannelMessage channelMessage) { if (string.IsNullOrEmpty( MessagingRedisCacheOptions.InstanceName) || - message.StartsWith( + channelMessage.Message.StartsWith( MessagingRedisCacheOptions.InstanceName)) { - var key = message - .ToString()[(MessagingRedisCacheOptions.InstanceName?.Length ?? 0)..]; + var key = channelMessage.Message + .ToString()[ (MessagingRedisCacheOptions.InstanceName?.Length ?? 0).. ]; MemoryCache.Remove( key); - OnMessage?.Invoke( - this, - new OnMessageEventArgs(key)); + await MessagingRedisCacheOptions + .Events + .OnMessageRecieved + .Invoke(channelMessage) + .ConfigureAwait(false); } } } diff --git a/src/MessagingRedisCache/Messaging/KeyspaceMessageSubscriber.cs b/src/MessagingRedisCache/Messaging/KeyspaceMessageSubscriber.cs index 239542c..dfa11b3 100644 --- a/src/MessagingRedisCache/Messaging/KeyspaceMessageSubscriber.cs +++ b/src/MessagingRedisCache/Messaging/KeyspaceMessageSubscriber.cs @@ -14,8 +14,6 @@ internal class KeyspaceMessageSubscriber( memoryCache; public MessagingRedisCacheOptions MessagingRedisCacheOptions { get; set; } = messagingRedisCacheOptionsAccessor.Value; - public EventHandler? OnMessage { get; set; } - public EventHandler? OnSubscribe { get; set; } private Regex ChannelPrefixRegex { get; } = new( @$"__keyspace@\d*__:{messagingRedisCacheOptionsAccessor.Value.InstanceName}"); @@ -24,18 +22,20 @@ public async Task SubscribeAsync( IConnectionMultiplexer connectionMultiplexer, CancellationToken cancellationToken = default) { - await connectionMultiplexer + (await connectionMultiplexer .GetSubscriber() .SubscribeAsync( new RedisChannel( $"__keyspace@*__:{MessagingRedisCacheOptions.InstanceName}*", - RedisChannel.PatternMode.Pattern), - ProcessMessage) - .ConfigureAwait(false); + RedisChannel.PatternMode.Pattern)) + .ConfigureAwait(false)) + .OnMessage(ProcessMessageAsync); - OnSubscribe?.Invoke( - this, - EventArgs.Empty); + await MessagingRedisCacheOptions + .Events + .OnSubscribe + .Invoke() + .ConfigureAwait(false); } public async Task UnsubscribeAsync( @@ -51,22 +51,23 @@ await connectionMultiplexer .ConfigureAwait(false); } - internal void ProcessMessage( - RedisChannel channel, - RedisValue message) + internal async Task ProcessMessageAsync( + ChannelMessage channelMessage) { - if (message == "del" || - message == "hset") + if (channelMessage.Message == "del" || + channelMessage.Message == "hset") { var key = ChannelPrefixRegex.Replace( - channel.ToString(), + channelMessage.Channel.ToString(), string.Empty); MemoryCache.Remove( key); - OnMessage?.Invoke( - this, - new OnMessageEventArgs(key)); + await MessagingRedisCacheOptions + .Events + .OnMessageRecieved + .Invoke(channelMessage) + .ConfigureAwait(false); } } } diff --git a/src/MessagingRedisCache/Messaging/NopMessageSubscriber.cs b/src/MessagingRedisCache/Messaging/NopMessageSubscriber.cs index 07d9bfb..61f9450 100644 --- a/src/MessagingRedisCache/Messaging/NopMessageSubscriber.cs +++ b/src/MessagingRedisCache/Messaging/NopMessageSubscriber.cs @@ -5,9 +5,6 @@ namespace MessagingRedisCache; internal sealed class NopMessageSubscriber : IMessageSubscriber { - public EventHandler? OnMessage { get; set; } - public EventHandler? OnSubscribe { get; set; } - public Task SubscribeAsync( IConnectionMultiplexer connectionMultiplexer, CancellationToken cancellationToken = default) diff --git a/src/MessagingRedisCache/Messaging/OnMessageEventArgs.cs b/src/MessagingRedisCache/Messaging/OnMessageEventArgs.cs deleted file mode 100644 index 3458e22..0000000 --- a/src/MessagingRedisCache/Messaging/OnMessageEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using StackExchange.Redis; - -namespace MessagingRedisCache; - -/// -/// Supplies information about a message event from an IMessageSubscriber. -/// -/// -/// Initializes a new instance of OnMessageEventArgs. -/// -public class OnMessageEventArgs( - RedisKey key) : - EventArgs -{ - /// - /// The cache key pertaining to the message event. - /// - public RedisKey Key { get; set; } = key; -} diff --git a/src/MessagingRedisCache/MessagingRedisCacheEvents.cs b/src/MessagingRedisCache/MessagingRedisCacheEvents.cs new file mode 100644 index 0000000..313a68c --- /dev/null +++ b/src/MessagingRedisCache/MessagingRedisCacheEvents.cs @@ -0,0 +1,21 @@ +using StackExchange.Redis; + +namespace MessagingRedisCache; + +/// +/// Specifies which events the MessagingRedisCache invokes. +/// +public class MessagingRedisCacheEvents +{ + /// + /// Invoked when a message is recieved. + /// + public Func OnMessageRecieved { get; set; } = + channelMessage => Task.CompletedTask; + + /// + /// Invoked when a message subscription is established. + /// + public Func OnSubscribe { get; set; } = + () => Task.CompletedTask; +} diff --git a/src/MessagingRedisCache/MessagingRedisCacheOptions.cs b/src/MessagingRedisCache/MessagingRedisCacheOptions.cs index bc3f8e5..9b70944 100644 --- a/src/MessagingRedisCache/MessagingRedisCacheOptions.cs +++ b/src/MessagingRedisCache/MessagingRedisCacheOptions.cs @@ -3,7 +3,7 @@ namespace MessagingRedisCache; /// -/// Configuration options for L1L2RedisCache. +/// Configuration options for MessagingRedisCache. /// public class MessagingRedisCacheOptions : RedisCacheOptions @@ -14,6 +14,12 @@ public class MessagingRedisCacheOptions : public string Channel => $"{InstanceName}{nameof(MessagingRedisCache)}"; + /// + /// Specifies which events the MessagingRedisCache invokes. + /// + public MessagingRedisCacheEvents Events { get; set; } = + new MessagingRedisCacheEvents(); + /// /// Unique identifier for the operating instance. /// diff --git a/tests/L1L2RedisCache/System/MessagingTests.cs b/tests/L1L2RedisCache/System/MessagingTests.cs index fe15ba1..041bcc1 100644 --- a/tests/L1L2RedisCache/System/MessagingTests.cs +++ b/tests/L1L2RedisCache/System/MessagingTests.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Text; namespace L1L2RedisCache.Tests.System; @@ -30,11 +29,21 @@ public async Task MessagingTypeTest( int iterations, MessagingType messagingType) { + using var messageAutoResetEvent = + new AutoResetEvent(false); + using var subscribeAutoResetEvent = + new AutoResetEvent(false); + var primaryServices = new ServiceCollection(); primaryServices.AddSingleton(Configuration); primaryServices.AddL1L2RedisCache(options => { Configuration.Bind("L1L2RedisCache", options); + options.Events.OnSubscribe = () => + { + subscribeAutoResetEvent.Set(); + return Task.CompletedTask; + }; options.MessagingType = messagingType; }); using var primaryServiceProvider = primaryServices @@ -56,24 +65,16 @@ await SetAndVerifyConfigurationAsync( secondaryServices.AddL1L2RedisCache(options => { Configuration.Bind("L1L2RedisCache", options); + options.Events.OnMessageRecieved = channelMessage => + { + messageAutoResetEvent.Set(); + return Task.CompletedTask; + }; options.MessagingType = messagingType; }); using var secondaryServiceProvider = secondaryServices .BuildServiceProvider(); - var secondaryMessageSubscriber = secondaryServiceProvider - .GetRequiredService(); - using var messageAutoResetEvent = new AutoResetEvent(false); - using var subscribeAutoResetEvent = new AutoResetEvent(false); - secondaryMessageSubscriber.OnMessage += (sender, e) => - { - messageAutoResetEvent.Set(); - }; - secondaryMessageSubscriber.OnSubscribe += (sender, e) => - { - subscribeAutoResetEvent.Set(); - }; - var secondaryL1Cache = secondaryServiceProvider .GetRequiredService(); var secondaryL1L2Cache = secondaryServiceProvider diff --git a/tests/L1L2RedisCache/System/PerformanceTests.cs b/tests/L1L2RedisCache/System/PerformanceTests.cs index 5becbf1..762814e 100644 --- a/tests/L1L2RedisCache/System/PerformanceTests.cs +++ b/tests/L1L2RedisCache/System/PerformanceTests.cs @@ -1,7 +1,6 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Extensions; using BenchmarkDotNet.Running; -using Microsoft.VisualStudio.TestTools.UnitTesting; namespace L1L2RedisCache.Tests.System; diff --git a/tests/L1L2RedisCache/System/ReliabilityTests.cs b/tests/L1L2RedisCache/System/ReliabilityTests.cs index 912029c..75e1397 100644 --- a/tests/L1L2RedisCache/System/ReliabilityTests.cs +++ b/tests/L1L2RedisCache/System/ReliabilityTests.cs @@ -1,8 +1,6 @@ -using MessagingRedisCache; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.TestTools.UnitTesting; using StackExchange.Redis; namespace L1L2RedisCache.Tests.System; @@ -23,24 +21,24 @@ public class ReliabilityTests( [TestMethod] public void InitializeBadConnectionTest() { + using var subscribeAutoResetEvent = + new AutoResetEvent(false); + var services = new ServiceCollection(); services.AddSingleton(Configuration); services.AddL1L2RedisCache(options => { Configuration.Bind("L1L2RedisCache", options); + options.Events.OnSubscribe = () => + { + subscribeAutoResetEvent.Set(); + return Task.CompletedTask; + }; options.Configuration = "localhost:80"; }); using var serviceProvider = services .BuildServiceProvider(); - var messageSubscriber = serviceProvider - .GetRequiredService(); - using var subscribeAutoResetEvent = new AutoResetEvent(false); - messageSubscriber.OnSubscribe += (sender, e) => - { - subscribeAutoResetEvent.Set(); - }; - var l1L2Cache = serviceProvider .GetRequiredService(); diff --git a/tests/MessagingRedisCache/System/DefaultMessagingTests.cs b/tests/MessagingRedisCache/System/DefaultMessagingTests.cs index b705f07..f000e95 100644 --- a/tests/MessagingRedisCache/System/DefaultMessagingTests.cs +++ b/tests/MessagingRedisCache/System/DefaultMessagingTests.cs @@ -1,5 +1,3 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - namespace MessagingRedisCache.Tests.System; [TestClass] diff --git a/tests/MessagingRedisCache/System/HybridCacheTests.cs b/tests/MessagingRedisCache/System/HybridCacheTests.cs index 9250f9d..61668a9 100644 --- a/tests/MessagingRedisCache/System/HybridCacheTests.cs +++ b/tests/MessagingRedisCache/System/HybridCacheTests.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.TestTools.UnitTesting; namespace MessagingRedisCache.Tests.System; @@ -17,6 +16,15 @@ public HybridCacheTests( { PrimaryServices.AddHybridCache(); SecondaryServices.AddHybridCache(); + + SecondaryOptionsConfigureAction = options => + { + options.Events.OnMessageRecieved = channelMessage => + { + MessageAutoResetEvent.Set(); + return Task.CompletedTask; + }; + }; } public HybridCacheEntryOptions HybridCacheEntryOptions { get; } = @@ -24,6 +32,8 @@ public HybridCacheTests( { Expiration = TimeSpan.FromHours(1), }; + public AutoResetEvent MessageAutoResetEvent { get; } = + new AutoResetEvent(false); public HybridCache PrimaryHybridCache => PrimaryServiceProvider .GetRequiredService(); @@ -43,14 +53,6 @@ public async Task GetOrCreateAsyncTest() await SetAndVerifyConfigurationAsync() .ConfigureAwait(false); - var secondaryMessageSubscriber = SecondaryServiceProvider - .GetRequiredService(); - using var messageAutoResetEvent = new AutoResetEvent(false); - secondaryMessageSubscriber.OnMessage += (sender, e) => - { - messageAutoResetEvent.Set(); - }; - for (var iteration = 0; iteration < Iterations; iteration++) { var key = Guid.NewGuid().ToString(); @@ -64,7 +66,7 @@ await PrimaryHybridCache cancellationToken: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( - messageAutoResetEvent + MessageAutoResetEvent .WaitOne(EventTimeout)); Assert.AreEqual( value, @@ -99,7 +101,7 @@ await PrimaryHybridCache cancellationToken: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( - messageAutoResetEvent + MessageAutoResetEvent .WaitOne(EventTimeout)); Assert.IsNull( PrimaryMemoryCache diff --git a/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs b/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs index 54448fc..59ff993 100644 --- a/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs +++ b/tests/MessagingRedisCache/System/KeyeventMessagingTests.cs @@ -1,5 +1,3 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - namespace MessagingRedisCache.Tests.System; [TestClass] diff --git a/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs b/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs index f67d7e9..eace898 100644 --- a/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs +++ b/tests/MessagingRedisCache/System/KeyspaceMessagingTests.cs @@ -1,5 +1,3 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - namespace MessagingRedisCache.Tests.System; [TestClass] diff --git a/tests/MessagingRedisCache/System/MessagingTestsBase.cs b/tests/MessagingRedisCache/System/MessagingTestsBase.cs index c25b024..2a931d7 100644 --- a/tests/MessagingRedisCache/System/MessagingTestsBase.cs +++ b/tests/MessagingRedisCache/System/MessagingTestsBase.cs @@ -1,21 +1,35 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Diagnostics.CodeAnalysis; using System.Text; namespace MessagingRedisCache.Tests.System; [TestClass] -public abstract class MessagingTestsBase( - MessagingType messagingType, - TestContext testContext) : - TestsBase( - messagingType, - testContext) +public abstract class MessagingTestsBase : + TestsBase { + protected MessagingTestsBase( + MessagingType messagingType, + TestContext testContext) : + base( + messagingType, + testContext) + { + SecondaryOptionsConfigureAction = options => + { + options.Events.OnMessageRecieved = channelMessage => + { + MessageAutoResetEvent.Set(); + return Task.CompletedTask; + }; + }; + } + + public AutoResetEvent MessageAutoResetEvent { get; } = + new AutoResetEvent(false); + [TestInitialize] public override void TestInitialize() { @@ -28,14 +42,6 @@ public async Task RemoveAsyncTest() await SetAndVerifyConfigurationAsync() .ConfigureAwait(false); - var secondaryMessageSubscriber = SecondaryServiceProvider - .GetRequiredService(); - using var messageAutoResetEvent = new AutoResetEvent(false); - secondaryMessageSubscriber.OnMessage += (sender, e) => - { - messageAutoResetEvent.Set(); - }; - for (var iteration = 0; iteration < Iterations; iteration++) { var key = Guid.NewGuid().ToString(); @@ -48,7 +54,7 @@ await PrimaryDistributedCache token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( - messageAutoResetEvent + MessageAutoResetEvent .WaitOne(EventTimeout)); SecondaryMemoryCache @@ -59,7 +65,7 @@ await PrimaryDistributedCache token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( - messageAutoResetEvent + MessageAutoResetEvent .WaitOne(EventTimeout)); Assert.IsNull( SecondaryMemoryCache @@ -74,14 +80,6 @@ public async Task RemoveTest() await SetAndVerifyConfigurationAsync() .ConfigureAwait(false); - var secondaryMessageSubscriber = SecondaryServiceProvider - .GetRequiredService(); - using var messageAutoResetEvent = new AutoResetEvent(false); - secondaryMessageSubscriber.OnMessage += (sender, e) => - { - messageAutoResetEvent.Set(); - }; - for (var iteration = 0; iteration < Iterations; iteration++) { var key = Guid.NewGuid().ToString(); @@ -94,7 +92,7 @@ await PrimaryDistributedCache token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( - messageAutoResetEvent + MessageAutoResetEvent .WaitOne(EventTimeout)); SecondaryMemoryCache @@ -103,7 +101,7 @@ await PrimaryDistributedCache .Remove( key); Assert.IsTrue( - messageAutoResetEvent + MessageAutoResetEvent .WaitOne(EventTimeout)); Assert.IsNull( SecondaryMemoryCache @@ -117,14 +115,6 @@ public async Task SetAsyncTest() await SetAndVerifyConfigurationAsync() .ConfigureAwait(false); - var secondaryMessageSubscriber = SecondaryServiceProvider - .GetRequiredService(); - using var messageAutoResetEvent = new AutoResetEvent(false); - secondaryMessageSubscriber.OnMessage += (sender, e) => - { - messageAutoResetEvent.Set(); - }; - for (var iteration = 0; iteration < Iterations; iteration++) { var key = Guid.NewGuid().ToString(); @@ -140,7 +130,7 @@ await PrimaryDistributedCache token: TestContext.CancellationToken) .ConfigureAwait(false); Assert.IsTrue( - messageAutoResetEvent + MessageAutoResetEvent .WaitOne(EventTimeout)); Assert.IsNull( SecondaryMemoryCache @@ -155,14 +145,6 @@ public async Task SetTest() await SetAndVerifyConfigurationAsync() .ConfigureAwait(false); - var secondaryMessageSubscriber = SecondaryServiceProvider - .GetRequiredService(); - using var messageAutoResetEvent = new AutoResetEvent(false); - secondaryMessageSubscriber.OnMessage += (sender, e) => - { - messageAutoResetEvent.Set(); - }; - for (var iteration = 0; iteration < Iterations; iteration++) { var key = Guid.NewGuid().ToString(); @@ -176,7 +158,7 @@ await SetAndVerifyConfigurationAsync() Encoding.ASCII.GetBytes(value), DistributedCacheEntryOptions); Assert.IsTrue( - messageAutoResetEvent + MessageAutoResetEvent .WaitOne(EventTimeout)); Assert.IsNull( SecondaryMemoryCache diff --git a/tests/MessagingRedisCache/System/TestsBase.cs b/tests/MessagingRedisCache/System/TestsBase.cs index 8271d14..eb611fa 100644 --- a/tests/MessagingRedisCache/System/TestsBase.cs +++ b/tests/MessagingRedisCache/System/TestsBase.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.TestTools.UnitTesting; namespace MessagingRedisCache.Tests.System; @@ -24,7 +23,7 @@ internal TestsBase( Configuration.Bind("MessagingRedisCache", options); options.MessagingType = MessagingType; }) - .AddMemoryCacheSubscriber();; + .AddMemoryCacheSubscriber(); SecondaryServices = new ServiceCollection(); SecondaryServices.AddSingleton(Configuration); @@ -38,6 +37,9 @@ internal TestsBase( .AddMemoryCacheSubscriber(); } + public virtual Action? PrimaryOptionsConfigureAction { get; set; } + public virtual Action? SecondaryOptionsConfigureAction { get; set; } + public IConfiguration Configuration { get; } = new ConfigurationBuilder() .AddJsonFile("appsettings.json") @@ -66,6 +68,12 @@ internal TestsBase( [TestInitialize] public virtual void TestInitialize() { + if (PrimaryOptionsConfigureAction != null) + { + PrimaryServices + .Configure( + PrimaryOptionsConfigureAction); + } PrimaryServiceProvider = PrimaryServices .BuildServiceProvider(); PrimaryDistributedCache = PrimaryServiceProvider @@ -73,6 +81,12 @@ public virtual void TestInitialize() PrimaryMemoryCache = PrimaryServiceProvider .GetRequiredService(); + if (SecondaryOptionsConfigureAction != null) + { + SecondaryServices + .Configure( + SecondaryOptionsConfigureAction); + } SecondaryServiceProvider = SecondaryServices .BuildServiceProvider(); SecondaryDistributedCache = SecondaryServiceProvider diff --git a/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs b/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs index f7cd735..087f8bb 100644 --- a/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs +++ b/tests/MessagingRedisCache/Unit/MessagingRedisCacheTests.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; using StackExchange.Redis; From 4ba01a669bb92e0a310feeb2e0f3a0d6bf0315d5 Mon Sep 17 00:00:00 2001 From: null-d3v Date: Fri, 28 Nov 2025 12:37:57 -0600 Subject: [PATCH 13/14] chore: docs --- src/MessagingRedisCache/MessagingType.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MessagingRedisCache/MessagingType.cs b/src/MessagingRedisCache/MessagingType.cs index e79f451..15f33de 100644 --- a/src/MessagingRedisCache/MessagingType.cs +++ b/src/MessagingRedisCache/MessagingType.cs @@ -6,7 +6,7 @@ public enum MessagingType { /// - /// Use standard L1L2RedisCache pub/sub messages for L1 memory cache eviction. The Redis server requires no additional configuration. + /// Use standard MessagingRedisCache pub/sub messages for L1 memory cache eviction. The Redis server requires no additional configuration. /// Default = 0, From 67937e5fcae67c2cd67e3580be196134c0701018 Mon Sep 17 00:00:00 2001 From: Benjamin Soddy Date: Wed, 17 Dec 2025 16:12:28 -0600 Subject: [PATCH 14/14] chore: updates --- Directory.Packages.props | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 40e4b31..5b1563a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,15 +4,15 @@ true - - - - - - - - - + + + + + + + + +