├── .codecov.yml ├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md ├── PULL_REQUEST_TEMPLATE.md ├── renovate.json └── workflows │ └── build.yml ├── .gitignore ├── CONTRIBUTING.md ├── CacheTower.sln ├── CodeCoverage.runsettings ├── License.txt ├── README.md ├── benchmarks ├── CacheTower.AlternativesBenchmark │ ├── BaseBenchmark.cs │ ├── CacheAlternatives_File_Benchmark.cs │ ├── CacheAlternatives_Memory_Benchmark.cs │ ├── CacheAlternatives_Memory_Parallel_Benchmark.cs │ ├── CacheAlternatives_Redis_Benchmark.cs │ ├── CacheAlternatives_Redis_Parallel_Benchmark.cs │ ├── CacheTower.AlternativesBenchmark.csproj │ ├── Program.cs │ └── Utils │ │ └── RedisHelper.cs └── CacheTower.Benchmarks │ ├── CacheStackBenchmark.cs │ ├── CacheTower.Benchmarks.csproj │ ├── Extensions │ ├── BaseCacheChangeExtensionBenchmark.cs │ ├── BaseDistributedLockExtensionBenchmark.cs │ ├── BaseExtensionsBenchmark.cs │ └── Redis │ │ ├── RedisLockExtensionBenchmark.cs │ │ └── RedisRemoteEvictionExtensionBenchmark.cs │ ├── Program.cs │ ├── Providers │ ├── BaseCacheLayerBenchmark.cs │ ├── CacheLayerComparisonBenchmark.cs │ ├── Database │ │ └── MongoDbCacheLayerBenchmark.cs │ ├── FileSystem │ │ ├── BaseFileCacheLayerBenchmark.cs │ │ ├── NewtonsoftJsonFileCacheBenchmark.cs │ │ └── ProtobufFileCacheBenchmark.cs │ ├── Memory │ │ └── MemoryCacheBenchmark.cs │ └── Redis │ │ └── RedisCacheLayerBenchmark.cs │ └── Utils │ ├── MongoDbHelper.cs │ └── RedisHelper.cs ├── docs └── Comparison.md ├── images └── icon.png ├── src ├── CacheTower.Extensions.Redis │ ├── AssemblyInternals.cs │ ├── CacheTower.Extensions.Redis.csproj │ ├── RedisLockExtension.cs │ ├── RedisLockOptions.cs │ ├── RedisRemoteEvictionExtension.cs │ └── ServiceCollectionExtensions.cs ├── CacheTower.Providers.Database.MongoDB │ ├── CacheTower.Providers.Database.MongoDB.csproj │ ├── Commands │ │ ├── CleanupCommand.cs │ │ ├── EvictCommand.cs │ │ ├── FlushCommand.cs │ │ └── SetCommand.cs │ ├── Entities │ │ └── DbCachedEntry.cs │ ├── MongoDbCacheLayer.cs │ └── ServiceCollectionExtensions.cs ├── CacheTower.Providers.FileSystem.Json │ ├── CacheTower.Providers.FileSystem.Json.csproj │ └── JsonFileCacheLayer.cs ├── CacheTower.Providers.FileSystem.Protobuf │ ├── CacheTower.Providers.FileSystem.Protobuf.csproj │ └── ProtobufFileCacheLayer.cs ├── CacheTower.Providers.Redis │ ├── CacheTower.Providers.Redis.csproj │ ├── IsExternalInit.cs │ ├── RedisCacheLayer.cs │ ├── RedisCacheLayerOptions.cs │ └── ServiceCollectionExtensions.cs ├── CacheTower.Serializers.NewtonsoftJson │ ├── CacheTower.Serializers.NewtonsoftJson.csproj │ └── NewtonsoftJsonCacheSerializer.cs ├── CacheTower.Serializers.Protobuf │ ├── CacheTower.Serializers.Protobuf.csproj │ └── ProtobufCacheSerializer.cs ├── CacheTower.Serializers.SystemTextJson │ ├── CacheTower.Serializers.SystemTextJson.csproj │ └── SystemTextJsonCacheSerializer.cs ├── CacheTower │ ├── AssemblyInternals.cs │ ├── CacheContextActivators.cs │ ├── CacheEntry.cs │ ├── CacheEntryStatus.cs │ ├── CacheSettings.cs │ ├── CacheStack.cs │ ├── CacheStack{TContext}.cs │ ├── CacheTower.csproj │ ├── Extensions │ │ ├── AutoCleanupExtension.cs │ │ └── ExtensionContainer.cs │ ├── ICacheContextActivator.cs │ ├── ICacheContextScope.cs │ ├── ICacheExtension.cs │ ├── ICacheLayer.cs │ ├── ICacheSerializer.cs │ ├── ICacheStack.cs │ ├── ICacheStackAccessor.cs │ ├── Internal │ │ ├── CacheEntryKeyLock.cs │ │ ├── DateTimeProvider.cs │ │ └── MD5HashUtility.cs │ ├── IsExternalInit.cs │ ├── Providers │ │ ├── FileSystem │ │ │ ├── FileCacheLayer.cs │ │ │ ├── FileCacheLayerOptions.cs │ │ │ └── ManifestEntry.cs │ │ └── Memory │ │ │ └── MemoryCacheLayer.cs │ ├── Serializers │ │ └── CacheSerializationException.cs │ └── ServiceCollectionExtensions.cs └── Directory.Build.props └── tests └── CacheTower.Tests ├── CacheContextActivatorTests.cs ├── CacheEntryTests.cs ├── CacheStackContextTests.cs ├── CacheStackTests.cs ├── CacheTower.Tests.csproj ├── ComplexTypeCachingObjects.cs ├── Extensions ├── AutoCleanupExtensionTests.cs ├── ExtensionContainerTests.cs └── Redis │ ├── RedisLockExtensionTests.cs │ └── RedisRemoteEvictionExtensionTests.cs ├── FuncCacheContextActivator.cs ├── Providers ├── BaseCacheLayerTests.cs ├── Database │ └── MongoDbCacheLayerTests.cs ├── FileSystem │ └── FileCacheLayerTests.cs ├── Memory │ └── MemoryCacheLayerTests.cs └── Redis │ └── RedisCacheLayerTests.cs ├── Serializers ├── BaseSerializerTests.cs ├── NewtonsoftJsonCacheSerializerTests.cs ├── ProtobufCacheSerializerTests.cs └── SystemTextJsonCacheSerializerTests.cs ├── ServiceCollectionExtensionsTests.cs ├── TestBase.cs └── Utils ├── MongoDbHelper.cs └── RedisHelper.cs /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Based on the EditorConfig from Roslyn 2 | # top-most EditorConfig file 3 | root = true 4 | 5 | [*.cs] 6 | indent_style = tab 7 | 8 | # Sort using and Import directives with System.* appearing first 9 | dotnet_sort_system_directives_first = true 10 | # Avoid "this." and "Me." if not necessary 11 | dotnet_style_qualification_for_field = false:suggestion 12 | dotnet_style_qualification_for_property = false:suggestion 13 | dotnet_style_qualification_for_method = false:suggestion 14 | dotnet_style_qualification_for_event = false:suggestion 15 | 16 | # Use language keywords instead of framework type names for type references 17 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 18 | dotnet_style_predefined_type_for_member_access = true:suggestion 19 | 20 | # Suggest more modern language features when available 21 | dotnet_style_object_initializer = true:suggestion 22 | dotnet_style_collection_initializer = true:suggestion 23 | dotnet_style_coalesce_expression = true:suggestion 24 | dotnet_style_null_propagation = true:suggestion 25 | dotnet_style_explicit_tuple_names = true:suggestion 26 | 27 | # Prefer "var" everywhere 28 | csharp_style_var_for_built_in_types = true:suggestion 29 | csharp_style_var_when_type_is_apparent = true:suggestion 30 | csharp_style_var_elsewhere = true:suggestion 31 | 32 | # Prefer method-like constructs to have a block body 33 | csharp_style_expression_bodied_methods = false:none 34 | csharp_style_expression_bodied_constructors = false:none 35 | csharp_style_expression_bodied_operators = false:none 36 | 37 | # Prefer property-like constructs to have an expression-body 38 | csharp_style_expression_bodied_properties = when_on_single_line:suggestion 39 | csharp_style_expression_bodied_indexers = true:none 40 | csharp_style_expression_bodied_accessors = when_on_single_line:suggestion 41 | 42 | # Suggest more modern language features when available 43 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 44 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 45 | csharp_style_inlined_variable_declaration = true:suggestion 46 | csharp_style_throw_expression = true:suggestion 47 | csharp_style_conditional_delegate_call = true:suggestion 48 | 49 | # Newline settings 50 | csharp_new_line_before_open_brace = all 51 | csharp_new_line_before_else = true 52 | csharp_new_line_before_catch = true 53 | csharp_new_line_before_finally = true 54 | csharp_new_line_before_members_in_object_initializers = true 55 | csharp_new_line_before_members_in_anonymous_types = true 56 | 57 | # Misc 58 | csharp_space_after_keywords_in_control_flow_statements = true 59 | csharp_space_between_method_declaration_parameter_list_parentheses = false 60 | csharp_space_between_method_call_parameter_list_parentheses = false 61 | csharp_space_between_parentheses = false 62 | csharp_preserve_single_line_statements = false 63 | csharp_preserve_single_line_blocks = true 64 | csharp_indent_case_contents = true 65 | csharp_indent_switch_labels = true 66 | csharp_indent_labels = no_change 67 | 68 | # Custom naming conventions 69 | dotnet_naming_rule.non_field_members_must_be_capitalized.symbols = non_field_member_symbols 70 | dotnet_naming_symbols.non_field_member_symbols.applicable_kinds = property,method,event,delegate 71 | dotnet_naming_symbols.non_field_member_symbols.applicable_accessibilities = * 72 | 73 | dotnet_naming_rule.non_field_members_must_be_capitalized.style = pascal_case_style 74 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 75 | 76 | dotnet_naming_rule.non_field_members_must_be_capitalized.severity = suggestion -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Turnerj -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a bug report so it can be fixed! 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ### What does the bug do? 10 | 11 | This bug does X, Y and Z. 12 | 13 | ### How can it be reproduced? 14 | 15 | You can reproduce the bug by running this code... 16 | 17 | ```csharp 18 | // Your code goes here! 19 | ``` 20 | 21 | Or by following these steps: 22 | 23 | 1. ... 24 | 2. ... 25 | 3. ... 26 | 27 | ### Environment (if applicable) 28 | 29 | - NuGet Package Version: ... 30 | - .NET Runtime Version: .NET Framework 4.X / .NET Core X / .NET X 31 | - Operating System: Windows/Mac/Linux -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | ### What problem does the feature solve? 10 | 11 | If we add feature X, it allows people to do Y and Z. 12 | It is similar to feature Foo though also does Bar. 13 | 14 | ### How would you use/interact with the feature? (if applicable) 15 | 16 | How would you call it in your own code? 17 | Are there constraints that would need to be in place with this feature? -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>TurnerSoftware/.github:renovate-shared" 5 | ], 6 | "packageRules": [ 7 | { 8 | "matchSourceUrls": [ "https://github.com/dotnetcore/EasyCaching" ], 9 | "groupName": "EasyCaching monorepo packages" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | release: 8 | types: [ published ] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | env: 15 | # Disable the .NET logo in the console output. 16 | DOTNET_NOLOGO: true 17 | # Disable the .NET first time experience to skip caching NuGet packages and speed up the build. 18 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 19 | # Disable sending .NET CLI telemetry to Microsoft. 20 | DOTNET_CLI_TELEMETRY_OPTOUT: true 21 | 22 | BUILD_ARTIFACT_PATH: ${{github.workspace}}/build-artifacts 23 | 24 | jobs: 25 | build: 26 | name: Build ${{matrix.os}} 27 | runs-on: ${{matrix.os}} 28 | continue-on-error: ${{matrix.optional}} 29 | strategy: 30 | matrix: 31 | os: [ubuntu-latest, windows-latest] 32 | mongodb: ['6.0'] 33 | optional: [false] 34 | include: 35 | - os: ubuntu-latest 36 | ubuntu: 'jammy' 37 | - os: macOS-latest 38 | mongodb: '6.0' 39 | optional: true 40 | steps: 41 | # Configure Redis 42 | - name: Configure Redis (Ubuntu) 43 | if: matrix.os == 'ubuntu-latest' 44 | run: sudo apt-get install redis-server 45 | - name: Configure Redis (Windows) 46 | if: matrix.os == 'windows-latest' 47 | run: choco install Memurai-Developer 48 | - name: Configure Redis (MacOS) 49 | if: matrix.os == 'macOS-latest' 50 | run: brew install redis && brew services start redis 51 | 52 | # Configure MongoDB 53 | - name: Configure MongoDB (Ubuntu) 54 | if: matrix.os == 'ubuntu-latest' 55 | run: | 56 | wget -qO - https://www.mongodb.org/static/pgp/server-${{matrix.mongodb}}.asc | gpg --dearmor | sudo tee /usr/share/keyrings/mongodb.gpg > /dev/null 57 | echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb.gpg ] https://repo.mongodb.org/apt/ubuntu ${{matrix.ubuntu}}/mongodb-org/${{matrix.mongodb}} multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-${{matrix.mongodb}}.list 58 | sudo apt update 59 | sudo apt install mongodb-org 60 | sudo systemctl start mongod 61 | - name: Configure MongoDB (Windows) 62 | if: matrix.os == 'windows-latest' 63 | shell: powershell 64 | run: | 65 | $latestPackageVersion = Resolve-ChocoPackageVersion -TargetVersion ${{matrix.mongodb}} -PackageName "mongodb.install" 66 | choco install mongodb.portable --version=$latestPackageVersion 67 | - name: Configure MongoDB (MacOS) 68 | if: matrix.os == 'macOS-latest' 69 | run: | 70 | brew tap mongodb/brew 71 | brew update 72 | brew install mongodb-community@${{matrix.mongodb}} 73 | brew services start mongodb-community@${{matrix.mongodb}} 74 | 75 | - name: Checkout 76 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 77 | 78 | - name: Setup dotnet SDK 79 | uses: actions/setup-dotnet@v4 80 | with: 81 | dotnet-version: | 82 | 6.0.x 83 | 7.0.306 84 | - name: Install dependencies 85 | run: dotnet restore 86 | - name: Build 87 | run: dotnet build --no-restore -c Release 88 | - name: Test with Coverage 89 | run: dotnet test --no-restore --logger trx --results-directory ${{env.BUILD_ARTIFACT_PATH}}/coverage --collect "XPlat Code Coverage" --settings CodeCoverage.runsettings /p:SkipBuildVersioning=true 90 | - name: Pack 91 | run: dotnet pack --no-build -c Release /p:PackageOutputPath=${{env.BUILD_ARTIFACT_PATH}} 92 | - name: Publish artifacts 93 | uses: actions/upload-artifact@v4 94 | with: 95 | name: ${{matrix.os}} 96 | path: ${{env.BUILD_ARTIFACT_PATH}} 97 | 98 | coverage: 99 | name: Process code coverage 100 | runs-on: ubuntu-latest 101 | needs: build 102 | steps: 103 | - name: Checkout 104 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 105 | - name: Download coverage reports 106 | uses: actions/download-artifact@v4 107 | - name: Install ReportGenerator tool 108 | run: dotnet tool install -g dotnet-reportgenerator-globaltool 109 | - name: Prepare coverage reports 110 | run: reportgenerator -reports:*/coverage/*/coverage.cobertura.xml -targetdir:./ -reporttypes:Cobertura 111 | - name: Upload coverage report 112 | uses: codecov/codecov-action@v4.5.0 113 | with: 114 | file: Cobertura.xml 115 | fail_ci_if_error: false 116 | - name: Save combined coverage report as artifact 117 | uses: actions/upload-artifact@v4 118 | with: 119 | name: coverage-report 120 | path: Cobertura.xml 121 | 122 | push-to-github-packages: 123 | name: 'Push GitHub Packages' 124 | needs: build 125 | if: github.ref == 'refs/heads/main' || github.event_name == 'release' 126 | environment: 127 | name: 'GitHub Packages' 128 | url: https://github.com/TurnerSoftware/CacheTower/packages 129 | permissions: 130 | packages: write 131 | runs-on: ubuntu-latest 132 | steps: 133 | - name: 'Download build' 134 | uses: actions/download-artifact@v4 135 | with: 136 | name: 'ubuntu-latest' 137 | - name: 'Add NuGet source' 138 | run: dotnet nuget add source https://nuget.pkg.github.com/TurnerSoftware/index.json --name GitHub --username Turnerj --password ${{secrets.GITHUB_TOKEN}} --store-password-in-clear-text 139 | - name: 'Upload NuGet package' 140 | run: dotnet nuget push *.nupkg --api-key ${{secrets.GH_PACKAGE_REGISTRY_API_KEY}} --source GitHub --skip-duplicate 141 | 142 | push-to-nuget: 143 | name: 'Push NuGet Packages' 144 | needs: build 145 | if: github.event_name == 'release' 146 | environment: 147 | name: 'NuGet' 148 | url: https://www.nuget.org/packages/CacheTower 149 | runs-on: ubuntu-latest 150 | steps: 151 | - name: 'Download build' 152 | uses: actions/download-artifact@v4 153 | with: 154 | name: 'ubuntu-latest' 155 | - name: 'Upload NuGet package' 156 | run: dotnet nuget push *.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{secrets.NUGET_API_KEY}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | # Coderush 255 | /.cr 256 | 257 | **/build-artifacts -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 🗺 Choose Your Own Contributing Adventure 2 | 3 | Did you [**find a bug**](#bug), have an idea for a [**new feature or change**](#idea) or just [**want to help out**](#help-out)? 4 | 5 |



6 | 7 | ## 🐛 I found a bug! 8 | 9 | Great! You can either open a [**bug report**](#bug-report) or submit a [**fix**](#fix-bug) for it. 10 | Both are useful options for maintaining this project - choose whichever you feel comfortable doing. 11 | 12 |

13 | 14 | ###
✍ I'll report the bug! 15 | 16 | Detailed issues are very helpful for tracking down the source of the problem. 17 | We don't want double-ups of issues so make sure you check that there isn't [an existing issue already open](https://github.com/TurnerSoftware/CacheTower/issues)! 18 | 19 | If there are no existing issues match the bug you've found, you'll need to write out a new one. 20 | The more useful the information you provide, the faster the problem can be solved. 21 | 22 | Ideally you would want to include: 23 | 24 | - NuGet package version 25 | - .NET runtime version 26 | - Steps to reproduce the issue 27 | - A _minimal_, reproducible example 28 | - Operating system (Windows/Linux/Mac) 29 | 30 | If you think you're ready, [**submit your bug report**](https://github.com/TurnerSoftware/CacheTower/issues/new?labels=bug&template=BUG_REPORT.md)! 31 | 32 |

33 | 34 | ###
💻 I'll fix the bug! 35 | 36 | That's great to hear! Here are some tips for a helpful bug fix: 37 | 38 | - It is a good idea to add a test that triggers this specific bug so we can confirm the fix works (also helpful for preventing regressions later on) 39 | - When developing a fix, make sure you follow the coding styles you see within the repository otherwise you might have to redo the changes! 40 | - If there is an issue open for the bug, make sure to tag that issue in your PR description 41 | 42 | You probably want to run the tests locally too. 43 | Cache Tower has [some requirements for local testing](#requirements-for-local-testing) which may affect your ability to run the full test suite. 44 | 45 | Got that bug fixed? [**Submit your pull request**](https://github.com/TurnerSoftware/CacheTower/compare)! 46 | 47 |





48 | 49 | ##
💡 I've got an idea for a new feature or change! 50 | 51 | Features are great! Is yours a [**small feature**](#idea-small) or a [**big feature**](#idea-big)? 52 | Both are welcome though smaller features are likely to be handled quicker than bigger features. 53 | 54 |

55 | 56 | ###
🤏 It's a small feature 57 | 58 | Small features usually don't take much time on the maintainer's side and not a lot of time on your side. 59 | Do you want to [**suggest the feature**](#idea-small-suggestion) or try a hand at [**implementing the feature**](#idea-small-implementation)? 60 | 61 |
62 | 63 | ####
✍ I'll suggest the small feature 64 | 65 | Nothing wrong with suggesting a feature! 66 | Keep in mind though that there is only so many hours in the day - even small features may take a while before they are reviewed. 67 | 68 | Sometimes features just aren't meant to be and won't get implemented. 69 | It isn't a personal statement if your feature isn't implemented - it might simply not fit in with "the vision" of the project or even conflict with planned changes. 70 | 71 | Here are some tips for a good small feature suggestion: 72 | 73 | - Describe what problem the feature is solving 74 | - Show an example of how you might use/interact with the feature 75 | 76 | If you still want to go ahead, [**submit your feature request**](https://github.com/TurnerSoftware/CacheTower/issues/new?labels=enhancement&template=FEATURE_REQUEST.md)! 77 | 78 |
79 | 80 | ####
💻 I'll implement the small feature 81 | 82 | Nice! Here are some tips for a useful feature implementation: 83 | 84 | - Features are only as good as the documentation around them. Make sure the documentation is updated appropriately. 85 | - Please add tests! It doesn't need to be perfect code coverage but the bulk behaviour of the change should be tested. 86 | - Keep to the coding styles you see within the repository otherwise you might have to redo the changes! 87 | - If there is an issue open for the feature, make sure to tag that issue in your PR description 88 | 89 | You probably want to run the tests locally too. 90 | Cache Tower has [some requirements for local testing](#requirements-for-local-testing) which may affect your ability to run the full test suite. 91 | 92 | Finished your implementation? [**Submit your pull request**](https://github.com/TurnerSoftware/CacheTower/compare)! 93 | 94 |

95 | 96 | ###
🙌 It's a big feature! 97 | 98 | Big features can either be awesome for a project or a burden to it. 99 | It is **highly** recommended to raise an issue about a big feature rather than open a pull request for it. 100 | 101 | Big features have to be carefully considered - whether they fit in with "the vision" of the project or potentially conflict with planned changes. 102 | Architectural decisions about the implementation may also need to be discussed to avoid breaking changes or performance penalities. 103 | 104 | With these things in mind, a big feature could be pending for months or may _never_ be implemented. 105 | 106 | Here are some tips for a good big feature suggestion: 107 | 108 | - Describe what problem the feature is solving 109 | - Show an example of how you might use/interact with the feature 110 | 111 | If you still want to go ahead, [**submit your feature request**](https://github.com/TurnerSoftware/CacheTower/issues/new?labels=enhancement&template=FEATURE_REQUEST.md)! 112 | 113 |





114 | 115 | ##
🙋‍ I just want to help out! 116 | 117 | Helpers are always welcome! Feel free to [**triage any open issues**](https://github.com/TurnerSoftware/CacheTower/issues) or make sure the documentation is up-to-date! 118 | 119 | If you want to do some coding, you could [**implement any open small suggested features**](#idea-small-implementation) which can bring the idea to reality. 120 | 121 | 122 | 123 | 124 |


125 |


126 |


127 | 128 | ## Miscellaneous 129 | 130 | ### Requirements for Local Testing 131 | 132 | Cache Tower uses external services to perform integration testing. 133 | To run all of the tests, you will need both Redis (or [compatible software](https://www.memurai.com/)) and MongoDB installed. 134 | 135 | For Redis, it needs to be at least version 5 compatible. 136 | The tests use the default connection of `localhost:6379` but can be overriden by environment variable `REDIS_ENDPOINT`. 137 | 138 | For MongoDB, it needs to be at least version 3. 139 | The tests use the default connection string of `mongodb://localhost` but can be overridden by environment variable `MONGODB_URI`. 140 | 141 | Back to [**fixing a bug**](#bug-fix) or [**implementing a small feature**](#idea-small-implementation). 142 | 143 |


144 |


-------------------------------------------------------------------------------- /CodeCoverage.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | cobertura 8 | [CacheTower.Tests]*,[CacheTower.Benchmarks]*,[CacheTower.AlternativesBenchmark]* 9 | [CacheTower]*,[CacheTower.*]* 10 | Obsolete 11 | true 12 | true 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Turner Software 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.AlternativesBenchmark/BaseBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Columns; 3 | using BenchmarkDotNet.Configs; 4 | using BenchmarkDotNet.Diagnosers; 5 | using BenchmarkDotNet.Environments; 6 | using BenchmarkDotNet.Jobs; 7 | using BenchmarkDotNet.Loggers; 8 | using BenchmarkDotNet.Order; 9 | using BenchmarkDotNet.Validators; 10 | 11 | namespace CacheTower.AlternativesBenchmark; 12 | 13 | [Config(typeof(Config))] 14 | public abstract class BaseBenchmark 15 | { 16 | public class Config : ManualConfig 17 | { 18 | public Config() 19 | { 20 | AddLogger(ConsoleLogger.Default); 21 | 22 | AddDiagnoser(MemoryDiagnoser.Default); 23 | AddColumn(StatisticColumn.OperationsPerSecond); 24 | AddColumnProvider(DefaultColumnProviders.Instance); 25 | 26 | WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest)); 27 | 28 | AddValidator(JitOptimizationsValidator.FailOnError); 29 | 30 | AddJob(Job.Default 31 | .WithRuntime(CoreRuntime.Core60) 32 | .WithMaxIterationCount(200)); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_File_Benchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using BenchmarkDotNet.Attributes; 6 | using CacheTower.Providers.FileSystem; 7 | using CacheTower.Serializers.NewtonsoftJson; 8 | using CacheTower.Serializers.Protobuf; 9 | using CacheTower.Serializers.SystemTextJson; 10 | using EasyCaching.Disk; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace CacheTower.AlternativesBenchmark 14 | { 15 | public class CacheAlternatives_File_Benchmark : BaseBenchmark 16 | { 17 | private const string DirectoryPath = "CacheAlternatives/FileCache"; 18 | 19 | private readonly CacheStack CacheTowerNewtonsoftJson; 20 | private readonly CacheStack CacheTowerSystemTextJson; 21 | private readonly CacheStack CacheTowerProtobuf; 22 | private DefaultDiskCachingProvider EasyCaching; 23 | 24 | public CacheAlternatives_File_Benchmark() 25 | { 26 | CacheTowerNewtonsoftJson = new CacheStack(null, new(new[] { new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance)) })); 27 | CacheTowerSystemTextJson = new CacheStack(null, new(new[] { new FileCacheLayer(new(DirectoryPath, SystemTextJsonCacheSerializer.Instance)) })); 28 | CacheTowerProtobuf = new CacheStack(null, new(new[] { new FileCacheLayer(new(DirectoryPath, ProtobufCacheSerializer.Instance)) })); 29 | } 30 | 31 | private static void CleanupFileSystem() 32 | { 33 | var attempts = 0; 34 | while (attempts < 5) 35 | { 36 | try 37 | { 38 | if (Directory.Exists(DirectoryPath)) 39 | { 40 | Directory.Delete(DirectoryPath, true); 41 | } 42 | 43 | break; 44 | } 45 | catch 46 | { 47 | Thread.Sleep(200); 48 | } 49 | attempts++; 50 | } 51 | } 52 | 53 | [GlobalSetup] 54 | public void Setup() 55 | { 56 | CleanupFileSystem(); 57 | 58 | // Easy Caching seems to generate a folder structure at initialization - this is required to be established for benchmarking. 59 | EasyCaching = new DefaultDiskCachingProvider("EasyCaching", new[] { new EasyCaching.Serialization.Protobuf.DefaultProtobufSerializer("EasyCaching") }, new DiskOptions 60 | { 61 | DBConfig = new DiskDbOptions 62 | { 63 | BasePath = DirectoryPath 64 | } 65 | }, (ILoggerFactory)null); 66 | } 67 | 68 | [Benchmark(Baseline = true)] 69 | public async Task CacheTower_FileCacheLayer_NewtonsoftJson() 70 | { 71 | return await CacheTowerNewtonsoftJson.GetOrSetAsync("GetOrSet_TestKey", (old) => 72 | { 73 | return Task.FromResult("Hello World"); 74 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); 75 | } 76 | 77 | [Benchmark] 78 | public async Task CacheTower_FileCacheLayer_SystemTextJson() 79 | { 80 | return await CacheTowerSystemTextJson.GetOrSetAsync("GetOrSet_TestKey", (old) => 81 | { 82 | return Task.FromResult("Hello World"); 83 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); 84 | } 85 | 86 | [Benchmark] 87 | public async Task CacheTower_FileCacheLayer_Protobuf() 88 | { 89 | return await CacheTowerProtobuf.GetOrSetAsync("GetOrSet_TestKey", (old) => 90 | { 91 | return Task.FromResult("Hello World"); 92 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); 93 | } 94 | 95 | [Benchmark] 96 | public async Task EasyCaching_Disk() 97 | { 98 | return (await EasyCaching.GetAsync("GetOrSet_TestKey", () => Task.FromResult("Hello World"), TimeSpan.FromDays(1))).Value; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Benchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using BenchmarkDotNet.Attributes; 4 | using CacheManager.Core; 5 | using CacheTower.Providers.Memory; 6 | using EasyCaching.InMemory; 7 | using LazyCache; 8 | using Microsoft.Extensions.Logging; 9 | using ZiggyCreatures.Caching.Fusion; 10 | 11 | namespace CacheTower.AlternativesBenchmark 12 | { 13 | public class CacheAlternatives_Memory_Benchmark : BaseBenchmark 14 | { 15 | private readonly CacheStack CacheTower; 16 | private readonly ICacheManager CacheManager; 17 | private readonly DefaultInMemoryCachingProvider EasyCaching; 18 | private readonly CachingService LazyCache; 19 | private readonly FusionCache FusionCache; 20 | private readonly IntelligentHack.IntelligentCache.MemoryCache IntelligentCache; 21 | 22 | public CacheAlternatives_Memory_Benchmark() 23 | { 24 | CacheTower = new CacheStack(null, new(new[] { new MemoryCacheLayer() })); 25 | CacheManager = CacheFactory.Build(b => 26 | { 27 | b.WithMicrosoftMemoryCacheHandle(); 28 | }); 29 | EasyCaching = new DefaultInMemoryCachingProvider( 30 | "EasyCaching", 31 | new[] { new InMemoryCaching("EasyCaching", new InMemoryCachingOptions()) }, 32 | new InMemoryOptions(), 33 | (ILoggerFactory)null 34 | ); 35 | LazyCache = new CachingService(); 36 | FusionCache = new FusionCache(new FusionCacheOptions()); 37 | IntelligentCache = new IntelligentHack.IntelligentCache.MemoryCache("IntelligentCache"); 38 | } 39 | 40 | [Benchmark(Baseline = true)] 41 | public async Task CacheTower_MemoryCacheLayer() 42 | { 43 | return await CacheTower.GetOrSetAsync("GetOrSet_TestKey", (old) => 44 | { 45 | return Task.FromResult("Hello World"); 46 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); 47 | } 48 | 49 | [Benchmark] 50 | public string CacheManager_MicrosoftMemoryCache() 51 | { 52 | return CacheManager.GetOrAdd("GetOrSet_TestKey", (key) => 53 | { 54 | return new CacheItem(key, "Hello World"); 55 | }).Value; 56 | } 57 | 58 | [Benchmark] 59 | public string EasyCaching_InMemory() 60 | { 61 | return EasyCaching.Get("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1)).Value; 62 | } 63 | 64 | [Benchmark] 65 | public string LazyCache_MemoryProvider() 66 | { 67 | return LazyCache.GetOrAdd("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1)); 68 | } 69 | 70 | [Benchmark] 71 | public string FusionCache_MemoryProvider() 72 | { 73 | return FusionCache.GetOrSet("GetOrSet_TestKey", (cancellationToken) => "Hello World", TimeSpan.FromDays(1)); 74 | } 75 | 76 | [Benchmark] 77 | public string IntelligentCache_MemoryCache() 78 | { 79 | return IntelligentCache.GetSet("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1)); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Memory_Parallel_Benchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using BenchmarkDotNet.Attributes; 4 | using CacheManager.Core; 5 | using CacheTower.Providers.Memory; 6 | using EasyCaching.InMemory; 7 | using LazyCache; 8 | using Microsoft.Extensions.Logging; 9 | using ZiggyCreatures.Caching.Fusion; 10 | 11 | namespace CacheTower.AlternativesBenchmark 12 | { 13 | public class CacheAlternatives_Memory_Parallel_Benchmark : BaseBenchmark 14 | { 15 | private readonly int ParallelIterations = 1000; 16 | 17 | private readonly CacheStack CacheTower; 18 | private readonly ICacheManager CacheManager; 19 | private readonly DefaultInMemoryCachingProvider EasyCaching; 20 | private readonly CachingService LazyCache; 21 | private readonly FusionCache FusionCache; 22 | private readonly IntelligentHack.IntelligentCache.MemoryCache IntelligentCache; 23 | 24 | public CacheAlternatives_Memory_Parallel_Benchmark() 25 | { 26 | CacheTower = new CacheStack(null, new(new[] { new MemoryCacheLayer() })); 27 | CacheManager = CacheFactory.Build(b => 28 | { 29 | b.WithMicrosoftMemoryCacheHandle(); 30 | }); 31 | EasyCaching = new DefaultInMemoryCachingProvider( 32 | "EasyCaching", 33 | new[] { new InMemoryCaching("EasyCaching", new InMemoryCachingOptions()) }, 34 | new InMemoryOptions(), 35 | (ILoggerFactory)null 36 | ); 37 | LazyCache = new CachingService(); 38 | FusionCache = new FusionCache(new FusionCacheOptions()); 39 | IntelligentCache = new IntelligentHack.IntelligentCache.MemoryCache("IntelligentCache"); 40 | } 41 | 42 | [Benchmark(Baseline = true)] 43 | public void CacheTower_MemoryCacheLayer() 44 | { 45 | Parallel.For(0, ParallelIterations, async i => 46 | { 47 | await CacheTower.GetOrSetAsync("GetOrSet_TestKey", (old) => 48 | { 49 | return Task.FromResult("Hello World"); 50 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); 51 | }); 52 | } 53 | 54 | [Benchmark] 55 | public void CacheManager_MicrosoftMemoryCache() 56 | { 57 | Parallel.For(0, ParallelIterations, i => 58 | { 59 | var _ = CacheManager.GetOrAdd("GetOrSet_TestKey", (key) => 60 | { 61 | return new CacheItem(key, "Hello World"); 62 | }).Value; 63 | }); 64 | } 65 | 66 | [Benchmark] 67 | public void EasyCaching_InMemory() 68 | { 69 | Parallel.For(0, ParallelIterations, i => 70 | { 71 | _ = EasyCaching.Get("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1)).Value; 72 | }); 73 | } 74 | 75 | [Benchmark] 76 | public void LazyCache_MemoryProvider() 77 | { 78 | Parallel.For(0, ParallelIterations, i => 79 | { 80 | LazyCache.GetOrAdd("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1)); 81 | }); 82 | } 83 | 84 | [Benchmark] 85 | public void FusionCache_MemoryProvider() 86 | { 87 | Parallel.For(0, ParallelIterations, i => 88 | { 89 | FusionCache.GetOrSet("GetOrSet_TestKey", (cancellationToken) => "Hello World", TimeSpan.FromDays(1)); 90 | }); 91 | } 92 | 93 | [Benchmark] 94 | public void IntelligentCache_MemoryCache() 95 | { 96 | Parallel.For(0, ParallelIterations, i => 97 | { 98 | IntelligentCache.GetSet("GetOrSet_TestKey", () => "Hello World", TimeSpan.FromDays(1)); 99 | }); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Redis_Benchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using BenchmarkDotNet.Attributes; 4 | using CacheManager.Core; 5 | using CacheTower.AlternativesBenchmark.Utils; 6 | using CacheTower.Providers.Redis; 7 | using CacheTower.Serializers.Protobuf; 8 | using EasyCaching.Redis; 9 | using EasyCaching.Serialization.Protobuf; 10 | using Microsoft.Extensions.Logging; 11 | using ProtoBuf; 12 | 13 | namespace CacheTower.AlternativesBenchmark 14 | { 15 | public class CacheAlternatives_Redis_Benchmark : BaseBenchmark 16 | { 17 | private readonly CacheStack CacheTower; 18 | private readonly ICacheManager CacheManager; 19 | private readonly DefaultRedisCachingProvider EasyCaching; 20 | private readonly IntelligentHack.IntelligentCache.RedisCache IntelligentCache; 21 | 22 | public CacheAlternatives_Redis_Benchmark() 23 | { 24 | CacheTower = new CacheStack(null, new(new[] { new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance)) })); 25 | CacheManager = CacheFactory.Build(b => 26 | { 27 | b.WithRedisConfiguration("redisLocal", "localhost:6379,ssl=false"); 28 | b.WithRedisCacheHandle("redisLocal", true); 29 | b.WithProtoBufSerializer(); 30 | }); 31 | 32 | var easyCachingRedisOptions = new RedisOptions 33 | { 34 | DBConfig = new RedisDBOptions 35 | { 36 | Configuration = "localhost:6379,ssl=false" 37 | } 38 | }; 39 | EasyCaching = new DefaultRedisCachingProvider("EasyCaching", 40 | new[] { new RedisDatabaseProvider("EasyCaching", easyCachingRedisOptions) }, 41 | new[] { new DefaultProtobufSerializer("EasyCaching") }, 42 | easyCachingRedisOptions, 43 | (ILoggerFactory)null 44 | ); 45 | IntelligentCache = new IntelligentHack.IntelligentCache.RedisCache(RedisHelper.GetConnection(), string.Empty); 46 | } 47 | 48 | [GlobalSetup] 49 | public void Setup() 50 | { 51 | RedisHelper.FlushDatabase(); 52 | } 53 | 54 | [Benchmark(Baseline = true)] 55 | public async Task CacheTower_RedisCacheLayer() 56 | { 57 | return await CacheTower.GetOrSetAsync("GetOrSet_TestKey", (old) => 58 | { 59 | return Task.FromResult("Hello World"); 60 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); 61 | } 62 | 63 | [Serializable] 64 | [ProtoContract] 65 | public class ProtobufCacheItem 66 | { 67 | [ProtoMember(1)] 68 | public string Value { get; set; } 69 | } 70 | 71 | [Benchmark] 72 | public string CacheManager_Redis() 73 | { 74 | return CacheManager.GetOrAdd("GetOrSet_TestKey", (key) => 75 | { 76 | return new ProtobufCacheItem 77 | { 78 | Value = "Hello World" 79 | }; 80 | }).Value; 81 | } 82 | 83 | [Benchmark] 84 | public async Task EasyCaching_Redis() 85 | { 86 | return (await EasyCaching.GetAsync("GetOrSet_TestKey", () => Task.FromResult("Hello World"), TimeSpan.FromDays(1))).Value; 87 | } 88 | 89 | [Benchmark] 90 | public async Task IntelligentCache_Redis() 91 | { 92 | return await IntelligentCache.GetSetAsync("GetOrSet_TestKey", (cancellationToken) => Task.FromResult("Hello World"), TimeSpan.FromDays(1)); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.AlternativesBenchmark/CacheAlternatives_Redis_Parallel_Benchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using BenchmarkDotNet.Attributes; 5 | using CacheManager.Core; 6 | using CacheTower.AlternativesBenchmark.Utils; 7 | using CacheTower.Providers.Redis; 8 | using CacheTower.Serializers.Protobuf; 9 | using EasyCaching.Redis; 10 | using EasyCaching.Serialization.Protobuf; 11 | using Microsoft.Extensions.Logging; 12 | using ProtoBuf; 13 | 14 | namespace CacheTower.AlternativesBenchmark 15 | { 16 | public class CacheAlternatives_Redis_Parallel_Benchmark : BaseBenchmark 17 | { 18 | private readonly int ParallelIterations = 100; 19 | 20 | private readonly CacheStack CacheTower; 21 | private readonly ICacheManager CacheManager; 22 | private readonly DefaultRedisCachingProvider EasyCaching; 23 | private readonly IntelligentHack.IntelligentCache.RedisCache IntelligentCache; 24 | 25 | public CacheAlternatives_Redis_Parallel_Benchmark() 26 | { 27 | CacheTower = new CacheStack(null, new(new[] { new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance)) })); 28 | CacheManager = CacheFactory.Build(b => 29 | { 30 | b.WithRedisConfiguration("redisLocal", "localhost:6379,ssl=false"); 31 | b.WithRedisCacheHandle("redisLocal", true); 32 | b.WithProtoBufSerializer(); 33 | }); 34 | 35 | var easyCachingRedisOptions = new RedisOptions 36 | { 37 | DBConfig = new RedisDBOptions 38 | { 39 | Configuration = "localhost:6379,ssl=false" 40 | } 41 | }; 42 | EasyCaching = new DefaultRedisCachingProvider("EasyCaching", 43 | new[] { new RedisDatabaseProvider("EasyCaching", easyCachingRedisOptions) }, 44 | new[] { new DefaultProtobufSerializer("EasyCaching") }, 45 | easyCachingRedisOptions, 46 | (ILoggerFactory)null 47 | ); 48 | IntelligentCache = new IntelligentHack.IntelligentCache.RedisCache(RedisHelper.GetConnection(), string.Empty); 49 | } 50 | 51 | [GlobalSetup] 52 | public void Setup() 53 | { 54 | RedisHelper.FlushDatabase(); 55 | Thread.Sleep(TimeSpan.FromSeconds(5)); 56 | } 57 | 58 | [Benchmark(Baseline = true)] 59 | public void CacheTower_RedisCacheLayer() 60 | { 61 | Parallel.For(0, ParallelIterations, async i => 62 | { 63 | await CacheTower.GetOrSetAsync("GetOrSet_TestKey", (old) => 64 | { 65 | return Task.FromResult("Hello World"); 66 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); 67 | }); 68 | } 69 | 70 | [Serializable] 71 | [ProtoContract] 72 | public class ProtobufCacheItem 73 | { 74 | [ProtoMember(1)] 75 | public string Value { get; set; } 76 | } 77 | 78 | [Benchmark] 79 | public void CacheManager_Redis() 80 | { 81 | Parallel.For(0, ParallelIterations, i => 82 | { 83 | var _ = CacheManager.GetOrAdd("GetOrSet_TestKey", (key) => 84 | { 85 | return new ProtobufCacheItem 86 | { 87 | Value = "Hello World" 88 | }; 89 | }).Value; 90 | }); 91 | } 92 | 93 | [Benchmark] 94 | public void EasyCaching_Redis() 95 | { 96 | Parallel.For(0, ParallelIterations, async i => 97 | { 98 | var _ = (await EasyCaching.GetAsync("GetOrSet_TestKey", () => Task.FromResult("Hello World"), TimeSpan.FromDays(1))).Value; 99 | }); 100 | } 101 | 102 | [Benchmark] 103 | public void IntelligentCache_Redis() 104 | { 105 | Parallel.For(0, ParallelIterations, async i => 106 | { 107 | await IntelligentCache.GetSetAsync("GetOrSet_TestKey", (cancellationToken) => Task.FromResult("Hello World"), TimeSpan.FromDays(1)); 108 | }); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.AlternativesBenchmark/CacheTower.AlternativesBenchmark.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.AlternativesBenchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Running; 3 | 4 | namespace CacheTower.AlternativesBenchmark 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.AlternativesBenchmark/Utils/RedisHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using StackExchange.Redis; 5 | 6 | namespace CacheTower.AlternativesBenchmark.Utils 7 | { 8 | public static class RedisHelper 9 | { 10 | public static string Endpoint => Environment.GetEnvironmentVariable("REDIS_ENDPOINT") ?? "localhost:6379"; 11 | 12 | private static ConnectionMultiplexer Connection { get; set; } 13 | 14 | public static ConnectionMultiplexer GetConnection() 15 | { 16 | if (Connection == null) 17 | { 18 | var config = new ConfigurationOptions 19 | { 20 | AllowAdmin = true 21 | }; 22 | config.EndPoints.Add(Endpoint); 23 | Connection = ConnectionMultiplexer.Connect(config); 24 | } 25 | return Connection; 26 | } 27 | 28 | public static void FlushDatabase() 29 | { 30 | GetConnection().GetServer(Endpoint).FlushDatabase(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/CacheStackBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Columns; 5 | using BenchmarkDotNet.Configs; 6 | using BenchmarkDotNet.Diagnosers; 7 | using BenchmarkDotNet.Environments; 8 | using BenchmarkDotNet.Jobs; 9 | using CacheTower.Providers.Memory; 10 | using Perfolizer.Horology; 11 | 12 | namespace CacheTower.Benchmarks 13 | { 14 | [Config(typeof(ConfigSettings))] 15 | public class CacheStackBenchmark 16 | { 17 | [Params(100)] 18 | public int WorkIterations { get; set; } 19 | 20 | public class ConfigSettings : ManualConfig 21 | { 22 | public ConfigSettings() 23 | { 24 | AddJob(Job.Default.WithRuntime(CoreRuntime.Core60).WithMaxIterationCount(50)); 25 | AddDiagnoser(MemoryDiagnoser.Default); 26 | 27 | AddColumn(StatisticColumn.OperationsPerSecond); 28 | SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default 29 | .WithSizeUnit(SizeUnit.B) 30 | .WithTimeUnit(TimeUnit.Nanosecond); 31 | } 32 | } 33 | 34 | [Benchmark] 35 | public async Task Set() 36 | { 37 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }))) 38 | { 39 | for (var i = 0; i < WorkIterations; i++) 40 | { 41 | await cacheStack.SetAsync("Set", 15, TimeSpan.FromDays(1)); 42 | } 43 | } 44 | } 45 | [Benchmark] 46 | public async Task Set_TwoLayers() 47 | { 48 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer(), new MemoryCacheLayer() }))) 49 | { 50 | for (var i = 0; i < WorkIterations; i++) 51 | { 52 | await cacheStack.SetAsync("Set", 15, TimeSpan.FromDays(1)); 53 | } 54 | } 55 | } 56 | [Benchmark] 57 | public async Task Evict() 58 | { 59 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }))) 60 | { 61 | for (var i = 0; i < WorkIterations; i++) 62 | { 63 | await cacheStack.SetAsync("Evict", 15, TimeSpan.FromDays(1)); 64 | await cacheStack.EvictAsync("Evict"); 65 | } 66 | } 67 | } 68 | [Benchmark] 69 | public async Task Evict_TwoLayers() 70 | { 71 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer(), new MemoryCacheLayer() }))) 72 | { 73 | for (var i = 0; i < WorkIterations; i++) 74 | { 75 | await cacheStack.SetAsync("Evict", 15, TimeSpan.FromDays(1)); 76 | await cacheStack.EvictAsync("Evict"); 77 | } 78 | } 79 | } 80 | [Benchmark] 81 | public async Task Cleanup() 82 | { 83 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }))) 84 | { 85 | for (var i = 0; i < WorkIterations; i++) 86 | { 87 | await cacheStack.SetAsync("Cleanup", 15, TimeSpan.FromDays(1)); 88 | await cacheStack.CleanupAsync(); 89 | } 90 | } 91 | } 92 | [Benchmark] 93 | public async Task Cleanup_TwoLayers() 94 | { 95 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer(), new MemoryCacheLayer() }))) 96 | { 97 | for (var i = 0; i < WorkIterations; i++) 98 | { 99 | await cacheStack.SetAsync("Cleanup", 15, TimeSpan.FromDays(1)); 100 | await cacheStack.CleanupAsync(); 101 | } 102 | } 103 | } 104 | [Benchmark] 105 | public async Task GetMiss() 106 | { 107 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }))) 108 | { 109 | for (var i = 0; i < WorkIterations; i++) 110 | { 111 | await cacheStack.GetAsync("GetMiss"); 112 | } 113 | } 114 | } 115 | [Benchmark] 116 | public async Task GetHit() 117 | { 118 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }))) 119 | { 120 | await cacheStack.SetAsync("GetHit", 15, TimeSpan.FromDays(1)); 121 | 122 | for (var i = 0; i < WorkIterations; i++) 123 | { 124 | await cacheStack.GetAsync("GetHit"); 125 | } 126 | } 127 | } 128 | [Benchmark] 129 | public async Task GetOrSet_NeverStale() 130 | { 131 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }))) 132 | { 133 | for (var i = 0; i < WorkIterations; i++) 134 | { 135 | await cacheStack.GetOrSetAsync("GetOrSet", (old) => 136 | { 137 | return Task.FromResult(12); 138 | }, new CacheSettings(TimeSpan.FromDays(1), TimeSpan.FromDays(1))); 139 | } 140 | } 141 | } 142 | [Benchmark] 143 | public async Task GetOrSet_AlwaysStale() 144 | { 145 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }))) 146 | { 147 | for (var i = 0; i < WorkIterations; i++) 148 | { 149 | await cacheStack.GetOrSetAsync("GetOrSet", (old) => 150 | { 151 | return Task.FromResult(12); 152 | }, new CacheSettings(TimeSpan.FromDays(1))); 153 | } 154 | } 155 | } 156 | [Benchmark] 157 | public async Task GetOrSet_UnderLoad() 158 | { 159 | await using (var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }))) 160 | { 161 | await cacheStack.SetAsync("GetOrSet", new CacheEntry(15, DateTime.UtcNow.AddDays(-1))); 162 | 163 | Parallel.For(0, WorkIterations, async value => 164 | { 165 | await cacheStack.GetOrSetAsync("GetOrSet", async (old) => 166 | { 167 | await Task.Delay(30); 168 | return 12; 169 | }, new CacheSettings(TimeSpan.FromDays(1))); 170 | }); 171 | } 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/CacheTower.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Extensions/BaseCacheChangeExtensionBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using BenchmarkDotNet.Attributes; 4 | 5 | namespace CacheTower.Benchmarks.Extensions 6 | { 7 | public abstract class BaseCacheChangeExtensionBenchmark : BaseExtensionsBenchmark 8 | { 9 | public DateTime BenchmarkValue; 10 | 11 | protected override void SetupBenchmark() 12 | { 13 | BenchmarkValue = DateTime.UtcNow; 14 | } 15 | 16 | [Benchmark] 17 | public async Task OnCacheUpdate() 18 | { 19 | var extension = CacheExtension as ICacheChangeExtension; 20 | await extension.OnCacheUpdateAsync("OnCacheUpdate_CacheKey", BenchmarkValue, CacheUpdateType.AddOrUpdateEntry); 21 | } 22 | 23 | [Benchmark] 24 | public async Task OnCacheEviction() 25 | { 26 | var extension = CacheExtension as ICacheChangeExtension; 27 | await extension.OnCacheEvictionAsync("OnCacheEviction_CacheKey"); 28 | } 29 | 30 | [Benchmark] 31 | public async Task OnCacheFlush() 32 | { 33 | var extension = CacheExtension as ICacheChangeExtension; 34 | await extension.OnCacheFlushAsync(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Extensions/BaseDistributedLockExtensionBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using BenchmarkDotNet.Attributes; 3 | 4 | namespace CacheTower.Benchmarks.Extensions; 5 | 6 | public abstract class BaseDistributedLockExtensionBenchmark : BaseExtensionsBenchmark 7 | { 8 | [Benchmark] 9 | public async Task AwaitAccessAndRelease() 10 | { 11 | var extension = CacheExtension as IDistributedLockExtension; 12 | await using var _ = await extension.AwaitAccessAsync("RefreshValue"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Extensions/BaseExtensionsBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using BenchmarkDotNet.Attributes; 4 | using BenchmarkDotNet.Columns; 5 | using BenchmarkDotNet.Configs; 6 | using BenchmarkDotNet.Diagnosers; 7 | using BenchmarkDotNet.Environments; 8 | using BenchmarkDotNet.Jobs; 9 | using CacheTower.Providers.Memory; 10 | using Perfolizer.Horology; 11 | 12 | namespace CacheTower.Benchmarks.Extensions 13 | { 14 | [Config(typeof(ConfigSettings))] 15 | public abstract class BaseExtensionsBenchmark 16 | { 17 | public class ConfigSettings : ManualConfig 18 | { 19 | public ConfigSettings() 20 | { 21 | AddJob(Job.Default.WithRuntime(CoreRuntime.Core60).WithMaxIterationCount(200)); 22 | AddDiagnoser(MemoryDiagnoser.Default); 23 | 24 | SummaryStyle = new BenchmarkDotNet.Reports.SummaryStyle(CultureInfo, true, SizeUnit.B, TimeUnit.Nanosecond); 25 | } 26 | } 27 | 28 | protected ICacheExtension CacheExtension { get; set; } 29 | 30 | protected virtual void SetupBenchmark() { } 31 | protected virtual void CleanupBenchmark() { } 32 | 33 | protected static CacheStack CacheStack { get; } = new CacheStack(null, new(new[] { new MemoryCacheLayer() })); 34 | 35 | [GlobalSetup] 36 | public void Setup() 37 | { 38 | SetupBenchmark(); 39 | CacheExtension.Register(CacheStack); 40 | } 41 | 42 | [GlobalCleanup] 43 | public async Task CleanupAsync() 44 | { 45 | CleanupBenchmark(); 46 | await CacheStack.DisposeAsync(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Extensions/Redis/RedisLockExtensionBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CacheTower.Benchmarks.Utils; 3 | using CacheTower.Extensions.Redis; 4 | 5 | namespace CacheTower.Benchmarks.Extensions.Redis 6 | { 7 | public class RedisLockExtensionBenchmark : BaseDistributedLockExtensionBenchmark 8 | { 9 | protected override void SetupBenchmark() 10 | { 11 | base.SetupBenchmark(); 12 | 13 | CacheExtension = new RedisLockExtension(RedisHelper.GetConnection()); 14 | RedisHelper.FlushDatabase(); 15 | } 16 | 17 | protected override void CleanupBenchmark() 18 | { 19 | base.CleanupBenchmark(); 20 | RedisHelper.FlushDatabase(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Extensions/Redis/RedisRemoteEvictionExtensionBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CacheTower.Benchmarks.Utils; 3 | using CacheTower.Extensions.Redis; 4 | 5 | namespace CacheTower.Benchmarks.Extensions.Redis 6 | { 7 | public class RedisRemoteEvictionExtensionBenchmark : BaseCacheChangeExtensionBenchmark 8 | { 9 | protected override void SetupBenchmark() 10 | { 11 | base.SetupBenchmark(); 12 | 13 | CacheExtension = new RedisRemoteEvictionExtension(RedisHelper.GetConnection()); 14 | RedisHelper.FlushDatabase(); 15 | } 16 | 17 | protected override void CleanupBenchmark() 18 | { 19 | base.CleanupBenchmark(); 20 | RedisHelper.FlushDatabase(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using BenchmarkDotNet.Running; 3 | 4 | namespace CacheTower.Benchmarks 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Providers/BaseCacheLayerBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using BenchmarkDotNet.Attributes; 5 | using BenchmarkDotNet.Columns; 6 | using BenchmarkDotNet.Configs; 7 | using BenchmarkDotNet.Diagnosers; 8 | using BenchmarkDotNet.Environments; 9 | using BenchmarkDotNet.Jobs; 10 | using Perfolizer.Horology; 11 | 12 | namespace CacheTower.Benchmarks.Providers 13 | { 14 | [Config(typeof(ConfigSettings))] 15 | public abstract class BaseCacheLayerBenchmark 16 | { 17 | public class ConfigSettings : ManualConfig 18 | { 19 | public ConfigSettings() 20 | { 21 | AddJob(Job.Default.WithRuntime(CoreRuntime.Core60).WithMaxIterationCount(200)); 22 | AddDiagnoser(MemoryDiagnoser.Default); 23 | 24 | AddColumn(StatisticColumn.OperationsPerSecond); 25 | SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default 26 | .WithSizeUnit(SizeUnit.B) 27 | .WithTimeUnit(TimeUnit.Nanosecond); 28 | } 29 | } 30 | 31 | [Params(1, 100)] 32 | public int WorkIterations { get; set; } 33 | 34 | protected Func CacheLayerProvider { get; set; } 35 | 36 | protected static async Task DisposeOf(ICacheLayer cacheLayer) 37 | { 38 | if (cacheLayer is IDisposable disposableLayer) 39 | { 40 | disposableLayer.Dispose(); 41 | } 42 | else if (cacheLayer is IAsyncDisposable asyncDisposableLayer) 43 | { 44 | await asyncDisposableLayer.DisposeAsync(); 45 | } 46 | } 47 | 48 | [Benchmark] 49 | public async Task GetMiss() 50 | { 51 | var cacheLayer = CacheLayerProvider.Invoke(); 52 | for (var i = 0; i < WorkIterations; i++) 53 | { 54 | await cacheLayer.GetAsync("GetMiss"); 55 | } 56 | await DisposeOf(cacheLayer); 57 | } 58 | 59 | [Benchmark] 60 | public async Task GetHit() 61 | { 62 | var cacheLayer = CacheLayerProvider.Invoke(); 63 | await cacheLayer.SetAsync("GetHit", new CacheEntry(1, TimeSpan.FromDays(1))); 64 | for (var i = 0; i < WorkIterations; i++) 65 | { 66 | await cacheLayer.GetAsync("GetHit"); 67 | } 68 | await DisposeOf(cacheLayer); 69 | } 70 | 71 | [Benchmark] 72 | public async Task SetExisting() 73 | { 74 | var cacheLayer = CacheLayerProvider.Invoke(); 75 | await cacheLayer.SetAsync("SetExisting", new CacheEntry(1, TimeSpan.FromDays(1))); 76 | for (var i = 0; i < WorkIterations; i++) 77 | { 78 | await cacheLayer.SetAsync("SetExisting", new CacheEntry(1, TimeSpan.FromDays(1))); 79 | } 80 | await DisposeOf(cacheLayer); 81 | } 82 | 83 | [Benchmark] 84 | public async Task EvictMiss() 85 | { 86 | var cacheLayer = CacheLayerProvider.Invoke(); 87 | for (var i = 0; i < WorkIterations; i++) 88 | { 89 | await cacheLayer.EvictAsync("EvictMiss"); 90 | } 91 | await DisposeOf(cacheLayer); 92 | } 93 | 94 | [Benchmark] 95 | public async Task EvictHit() 96 | { 97 | var cacheLayer = CacheLayerProvider.Invoke(); 98 | for (var i = 0; i < WorkIterations; i++) 99 | { 100 | await cacheLayer.SetAsync("EvictHit", new CacheEntry(1, TimeSpan.FromDays(1))); 101 | await cacheLayer.EvictAsync("EvictHit"); 102 | } 103 | await DisposeOf(cacheLayer); 104 | } 105 | 106 | [Benchmark] 107 | public async Task Cleanup() 108 | { 109 | var expiredDate = DateTime.UtcNow.AddDays(-1); 110 | var cacheLayer = CacheLayerProvider.Invoke(); 111 | for (var i = 0; i < WorkIterations; i++) 112 | { 113 | await cacheLayer.SetAsync($"Cleanup_{i}", new CacheEntry(1, expiredDate)); 114 | } 115 | await cacheLayer.CleanupAsync(); 116 | await DisposeOf(cacheLayer); 117 | } 118 | 119 | [Benchmark] 120 | public async Task GetHitSimultaneous() 121 | { 122 | var cacheLayer = CacheLayerProvider.Invoke(); 123 | 124 | await cacheLayer.SetAsync("GetHitSimultaneous", new CacheEntry(1, TimeSpan.FromDays(1))); 125 | 126 | var tasks = new List(); 127 | 128 | for (var i = 0; i < WorkIterations; i++) 129 | { 130 | var task = cacheLayer.GetAsync("GetHitSimultaneous"); 131 | tasks.Add(task.AsTask()); 132 | } 133 | 134 | await Task.WhenAll(tasks); 135 | 136 | await DisposeOf(cacheLayer); 137 | } 138 | 139 | [Benchmark] 140 | public async Task SetExistingSimultaneous() 141 | { 142 | var cacheLayer = CacheLayerProvider.Invoke(); 143 | 144 | await cacheLayer.SetAsync("SetExistingSimultaneous", new CacheEntry(1, TimeSpan.FromDays(1))); 145 | 146 | var tasks = new List(); 147 | 148 | for (var i = 0; i < WorkIterations; i++) 149 | { 150 | var task = cacheLayer.SetAsync("SetExistingSimultaneous", new CacheEntry(1, TimeSpan.FromDays(1))); 151 | tasks.Add(task.AsTask()); 152 | } 153 | 154 | await Task.WhenAll(tasks); 155 | 156 | await DisposeOf(cacheLayer); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Providers/CacheLayerComparisonBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using BenchmarkDotNet.Attributes; 6 | using BenchmarkDotNet.Columns; 7 | using BenchmarkDotNet.Configs; 8 | using BenchmarkDotNet.Diagnosers; 9 | using BenchmarkDotNet.Environments; 10 | using BenchmarkDotNet.Jobs; 11 | using BenchmarkDotNet.Order; 12 | using CacheTower.Benchmarks.Utils; 13 | using CacheTower.Providers.Database.MongoDB; 14 | using CacheTower.Providers.FileSystem; 15 | using CacheTower.Providers.Memory; 16 | using CacheTower.Providers.Redis; 17 | using CacheTower.Serializers.NewtonsoftJson; 18 | using CacheTower.Serializers.Protobuf; 19 | using Perfolizer.Horology; 20 | using ProtoBuf; 21 | 22 | namespace CacheTower.Benchmarks.Providers 23 | { 24 | [Config(typeof(ConfigSettings))] 25 | public class CacheLayerComparisonBenchmark 26 | { 27 | public class ConfigSettings : ManualConfig 28 | { 29 | public ConfigSettings() 30 | { 31 | AddJob(Job.Default.WithRuntime(CoreRuntime.Core60)); 32 | AddDiagnoser(MemoryDiagnoser.Default); 33 | 34 | WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest)); 35 | 36 | AddColumn(StatisticColumn.OperationsPerSecond); 37 | SummaryStyle = BenchmarkDotNet.Reports.SummaryStyle.Default 38 | .WithSizeUnit(SizeUnit.B) 39 | .WithTimeUnit(TimeUnit.Nanosecond); 40 | } 41 | } 42 | 43 | 44 | [Params(1, 10)] 45 | public int WorkIterations { get; set; } 46 | 47 | [ProtoContract] 48 | private class ComplexType 49 | { 50 | [ProtoMember(1)] 51 | public string ExampleString { get; set; } 52 | [ProtoMember(2)] 53 | public int ExampleNumber { get; set; } 54 | [ProtoMember(3)] 55 | public DateTime ExampleDate { get; set; } 56 | [ProtoMember(4)] 57 | public Dictionary DictionaryOfNumbers { get; set; } 58 | } 59 | 60 | protected async ValueTask BenchmarkWork(ICacheLayer cacheLayer) 61 | { 62 | for (var iterationCount = 0; iterationCount < WorkIterations; iterationCount++) 63 | { 64 | //Get 100 misses 65 | for (var i = 0; i < 100; i++) 66 | { 67 | await cacheLayer.GetAsync("GetMiss_" + i); 68 | } 69 | 70 | var startDate = DateTime.UtcNow.AddDays(-50); 71 | 72 | //Set first 100 (simple type) 73 | for (var i = 0; i < 100; i++) 74 | { 75 | await cacheLayer.SetAsync("Comparison_" + i, new CacheEntry(1, startDate.AddDays(i) + TimeSpan.FromDays(1))); 76 | } 77 | //Set last 100 (complex type) 78 | for (var i = 100; i < 200; i++) 79 | { 80 | await cacheLayer.SetAsync("Comparison_" + i, new CacheEntry(new ComplexType 81 | { 82 | ExampleString = "Hello World", 83 | ExampleNumber = 42, 84 | ExampleDate = new DateTime(2000, 1, 1), 85 | DictionaryOfNumbers = new Dictionary() { { "A", 1 }, { "B", 2 }, { "C", 3 } } 86 | }, startDate.AddDays(i - 100) + TimeSpan.FromDays(1))); 87 | } 88 | 89 | //Get first 50 (simple type) 90 | for (var i = 0; i < 50; i++) 91 | { 92 | await cacheLayer.GetAsync("Comparison_" + i); 93 | } 94 | //Get last 50 (complex type) 95 | for (var i = 150; i < 200; i++) 96 | { 97 | await cacheLayer.GetAsync("Comparison_" + i); 98 | } 99 | 100 | //Evict middle 100 101 | for (var i = 50; i < 150; i++) 102 | { 103 | await cacheLayer.EvictAsync("Comparison_" + i); 104 | } 105 | 106 | //Cleanup outer 100 107 | await cacheLayer.CleanupAsync(); 108 | } 109 | } 110 | 111 | [GlobalSetup] 112 | public void Setup() 113 | { 114 | MongoDbHelper.DropDatabase(); 115 | } 116 | 117 | [Benchmark(Baseline = true)] 118 | public async Task MemoryCacheLayer() 119 | { 120 | var cacheLayer = new MemoryCacheLayer(); 121 | await BenchmarkWork(cacheLayer); 122 | } 123 | 124 | [Benchmark] 125 | public async Task JsonFileCacheLayer() 126 | { 127 | var directoryPath = "CacheLayerComparison/NewtonsoftJson"; 128 | await using (var cacheLayer = new FileCacheLayer(new FileCacheLayerOptions(directoryPath, NewtonsoftJsonCacheSerializer.Instance))) 129 | { 130 | await BenchmarkWork(cacheLayer); 131 | } 132 | Directory.Delete(directoryPath, true); 133 | } 134 | 135 | [Benchmark] 136 | public async Task ProtobufFileCacheLayer() 137 | { 138 | var directoryPath = "CacheLayerComparison/Protobuf"; 139 | await using (var cacheLayer = new FileCacheLayer(new FileCacheLayerOptions(directoryPath, ProtobufCacheSerializer.Instance))) 140 | { 141 | await BenchmarkWork(cacheLayer); 142 | } 143 | Directory.Delete(directoryPath, true); 144 | } 145 | 146 | [Benchmark] 147 | public async Task MongoDbCacheLayer() 148 | { 149 | var cacheLayer = new MongoDbCacheLayer(MongoDbHelper.GetConnection()); 150 | await BenchmarkWork(cacheLayer); 151 | await MongoDbHelper.DropDatabaseAsync(); 152 | } 153 | 154 | [Benchmark] 155 | public async Task RedisCacheLayer() 156 | { 157 | var cacheLayer = new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance)); 158 | await BenchmarkWork(cacheLayer); 159 | RedisHelper.FlushDatabase(); 160 | } 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Providers/Database/MongoDbCacheLayerBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CacheTower.Benchmarks.Utils; 3 | using CacheTower.Providers.Database.MongoDB; 4 | using MongoFramework; 5 | 6 | namespace CacheTower.Benchmarks.Providers.Database 7 | { 8 | public class MongoDbCacheLayerBenchmark : BaseCacheLayerBenchmark 9 | { 10 | private IMongoDbConnection Connection { get; set; } 11 | 12 | [GlobalSetup] 13 | public void Setup() 14 | { 15 | Connection = MongoDbHelper.GetConnection(); 16 | CacheLayerProvider = () => new MongoDbCacheLayer(Connection); 17 | MongoDbHelper.DropDatabase(); 18 | } 19 | 20 | [IterationCleanup] 21 | public void IterationCleanup() 22 | { 23 | MongoDbHelper.DropDatabase(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Providers/FileSystem/BaseFileCacheLayerBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading; 6 | using BenchmarkDotNet.Attributes; 7 | 8 | namespace CacheTower.Benchmarks.Providers.FileSystem 9 | { 10 | public abstract class BaseFileCacheLayerBenchmark : BaseCacheLayerBenchmark 11 | { 12 | protected string DirectoryPath { get; set; } 13 | 14 | private void CleanupFileSystem() 15 | { 16 | var attempts = 0; 17 | while (attempts < 5) 18 | { 19 | try 20 | { 21 | if (Directory.Exists(DirectoryPath)) 22 | { 23 | Directory.Delete(DirectoryPath, true); 24 | } 25 | 26 | break; 27 | } 28 | catch 29 | { 30 | Thread.Sleep(200); 31 | } 32 | attempts++; 33 | } 34 | } 35 | 36 | [IterationSetup] 37 | public void PreIterationDirectoryCleanup() 38 | { 39 | CleanupFileSystem(); 40 | } 41 | 42 | [IterationCleanup] 43 | public void PostIterationDirectoryCleanup() 44 | { 45 | CleanupFileSystem(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Providers/FileSystem/NewtonsoftJsonFileCacheBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CacheTower.Providers.FileSystem; 3 | using CacheTower.Serializers.NewtonsoftJson; 4 | 5 | namespace CacheTower.Benchmarks.Providers.FileSystem 6 | { 7 | public class NewtonsoftJsonFileCacheBenchmark : BaseFileCacheLayerBenchmark 8 | { 9 | [GlobalSetup] 10 | public void Setup() 11 | { 12 | DirectoryPath = "FileCache/NewtonsoftJson"; 13 | CacheLayerProvider = () => new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance)); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Providers/FileSystem/ProtobufFileCacheBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CacheTower.Providers.FileSystem; 3 | using CacheTower.Serializers.Protobuf; 4 | 5 | namespace CacheTower.Benchmarks.Providers.FileSystem 6 | { 7 | public class ProtobufFileCacheBenchmark : BaseFileCacheLayerBenchmark 8 | { 9 | [GlobalSetup] 10 | public void Setup() 11 | { 12 | DirectoryPath = "FileCache/Protobuf"; 13 | CacheLayerProvider = () => new FileCacheLayer(new(DirectoryPath, ProtobufCacheSerializer.Instance)); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Providers/Memory/MemoryCacheBenchmark.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using BenchmarkDotNet.Attributes; 6 | using CacheTower.Providers.Memory; 7 | 8 | namespace CacheTower.Benchmarks.Providers.Memory 9 | { 10 | public class MemoryCacheBenchmark : BaseCacheLayerBenchmark 11 | { 12 | [GlobalSetup] 13 | public void Setup() 14 | { 15 | CacheLayerProvider = () => new MemoryCacheLayer(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Providers/Redis/RedisCacheLayerBenchmark.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using CacheTower.Benchmarks.Utils; 3 | using CacheTower.Providers.Redis; 4 | using CacheTower.Serializers.Protobuf; 5 | 6 | namespace CacheTower.Benchmarks.Providers.Redis 7 | { 8 | public class RedisCacheLayerBenchmark : BaseCacheLayerBenchmark 9 | { 10 | [GlobalSetup] 11 | public void Setup() 12 | { 13 | CacheLayerProvider = () => new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance)); 14 | } 15 | 16 | [IterationSetup] 17 | public void PreIterationRedisCleanup() 18 | { 19 | RedisHelper.FlushDatabase(); 20 | } 21 | 22 | [IterationCleanup] 23 | public void PostIterationRedisCleanup() 24 | { 25 | RedisHelper.FlushDatabase(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Utils/MongoDbHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using MongoDB.Driver; 4 | using MongoFramework; 5 | 6 | namespace CacheTower.Benchmarks.Utils 7 | { 8 | public static class MongoDbHelper 9 | { 10 | public static string ConnectionString => Environment.GetEnvironmentVariable("MONGODB_URI") ?? "mongodb://localhost"; 11 | 12 | public static string GetDatabaseName() 13 | { 14 | return "CacheTowerBenchmarks"; 15 | } 16 | 17 | public static IMongoDbConnection GetConnection() 18 | { 19 | var urlBuilder = new MongoUrlBuilder(ConnectionString) 20 | { 21 | DatabaseName = GetDatabaseName() 22 | }; 23 | return MongoDbConnection.FromUrl(urlBuilder.ToMongoUrl()); 24 | } 25 | 26 | public static async Task DropDatabaseAsync() 27 | { 28 | await GetConnection().Client.DropDatabaseAsync(GetDatabaseName()); 29 | } 30 | public static void DropDatabase() 31 | { 32 | GetConnection().Client.DropDatabase(GetDatabaseName()); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /benchmarks/CacheTower.Benchmarks/Utils/RedisHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using StackExchange.Redis; 3 | 4 | namespace CacheTower.Benchmarks.Utils 5 | { 6 | public static class RedisHelper 7 | { 8 | public static string Endpoint => Environment.GetEnvironmentVariable("REDIS_ENDPOINT") ?? "localhost:6379"; 9 | 10 | private static ConnectionMultiplexer Connection { get; set; } 11 | 12 | public static ConnectionMultiplexer GetConnection() 13 | { 14 | if (Connection == null) 15 | { 16 | var config = new ConfigurationOptions 17 | { 18 | AllowAdmin = true, 19 | SyncTimeout = (int)TimeSpan.FromSeconds(20).TotalMilliseconds 20 | }; 21 | config.EndPoints.Add(Endpoint); 22 | Connection = ConnectionMultiplexer.Connect(config); 23 | } 24 | return Connection; 25 | } 26 | 27 | public static void FlushDatabase() 28 | { 29 | GetConnection().GetServer(Endpoint).FlushDatabase(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/Comparison.md: -------------------------------------------------------------------------------- 1 | # Caching Performance Comparison 2 | 3 | **Test Machine** 4 | 5 | ``` 6 | BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1766 (21H1/May2021Update) 7 | Intel Core i7-6700HQ CPU 2.60GHz (Skylake), 1 CPU, 8 logical and 4 physical cores 8 | .NET SDK=6.0.300 9 | [Host] : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT 10 | Job-BJQIPU : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT 11 | 12 | Runtime=.NET 6.0 MaxIterationCount=200 13 | ``` 14 | 15 | _Note: The performance figures below are as a guide only. Different systems and configurations can drastically change performance results._ 16 | 17 | ## Sequential In-Memory Caching 18 | 19 | | Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Gen 0 | Allocated | 20 | |---------------------------------- |---------:|--------:|--------:|------------:|------:|--------:|-------:|----------:| 21 | | IntelligentCache_MemoryCache | 177.1 ns | 3.43 ns | 3.36 ns | 5,647,113.9 | 0.78 | 0.02 | 0.0279 | 88 B | 22 | | CacheTower_MemoryCacheLayer | 226.5 ns | 4.35 ns | 4.28 ns | 4,415,623.1 | 1.00 | 0.00 | 0.0229 | 72 B | 23 | | CacheManager_MicrosoftMemoryCache | 263.9 ns | 5.12 ns | 5.26 ns | 3,789,315.3 | 1.16 | 0.03 | 0.0277 | 88 B | 24 | | FusionCache_MemoryProvider | 295.7 ns | 5.60 ns | 5.50 ns | 3,382,054.7 | 1.31 | 0.04 | 0.1016 | 320 B | 25 | | LazyCache_MemoryProvider | 297.3 ns | 4.90 ns | 5.45 ns | 3,363,940.7 | 1.31 | 0.04 | 0.1144 | 360 B | 26 | | EasyCaching_InMemory | 301.1 ns | 5.86 ns | 7.41 ns | 3,321,052.2 | 1.33 | 0.03 | 0.0482 | 152 B | 27 | 28 | ## Parallel In-Memory Caching 29 | 30 | | Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Gen 0 | Allocated | 31 | |---------------------------------- |----------:|----------:|----------:|---------:|------:|--------:|---------:|----------:| 32 | | IntelligentCache_MemoryCache | 66.12 us | 0.479 us | 0.425 us | 15,124.8 | 0.89 | 0.01 | 29.2969 | 89 KB | 33 | | CacheTower_MemoryCacheLayer | 74.29 us | 1.292 us | 1.208 us | 13,460.5 | 1.00 | 0.00 | 0.9766 | 3 KB | 34 | | CacheManager_MicrosoftMemoryCache | 93.83 us | 0.639 us | 0.566 us | 10,657.6 | 1.27 | 0.02 | 29.2969 | 89 KB | 35 | | EasyCaching_InMemory | 119.71 us | 1.501 us | 1.404 us | 8,353.7 | 1.61 | 0.02 | 49.9268 | 151 KB | 36 | | FusionCache_MemoryProvider | 132.27 us | 0.775 us | 0.605 us | 7,560.2 | 1.79 | 0.02 | 104.2480 | 316 KB | 37 | | LazyCache_MemoryProvider | 917.56 us | 16.636 us | 15.561 us | 1,089.9 | 12.35 | 0.14 | 118.1641 | 356 KB | 38 | 39 | 40 | ## Sequential Redis Caching 41 | 42 | - Redis benchmarks unable to run due to [bug in StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis/pull/2166). 43 | The results below are from an older version of .NET 44 | 45 | | Method | Mean | Error | StdDev | Op/s | Ratio | Gen 0 | Gen 1 | Gen 2 | Allocated | 46 | |--------------------------- |---------:|--------:|--------:|--------:|------:|-------:|------:|------:|----------:| 47 | | CacheManager_Redis | 134.7 us | 0.72 us | 0.67 us | 7,425.3 | 0.84 | 0.7324 | - | - | 2376 B | 48 | | IntelligentCache_Redis | 156.2 us | 0.91 us | 0.85 us | 6,403.4 | 0.97 | 0.9766 | - | - | 3456 B | 49 | | EasyCaching_Redis | 158.1 us | 0.50 us | 0.47 us | 6,326.3 | 0.99 | 0.2441 | - | - | 1144 B | 50 | | CacheTower_RedisCacheLayer | 160.3 us | 0.47 us | 0.44 us | 6,238.4 | 1.00 | 0.2441 | - | - | 936 B | 51 | 52 | ## Parallel Redis Caching 53 | 54 | - Redis benchmarks unable to run due to [bug in StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis/pull/2166) 55 | The results below are from an older version of .NET 56 | 57 | | Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | 58 | |--------------------------- |-----------:|---------:|---------:|--------:|------:|--------:|--------:|--------:|-------:|----------:| 59 | | CacheTower_RedisCacheLayer | 413.2 us | 9.73 us | 40.90 us | 2,420.0 | 1.00 | 0.00 | 24.4141 | 9.7656 | 1.9531 | 110.38 KB | 60 | | EasyCaching_Redis | 452.3 us | 8.99 us | 31.63 us | 2,211.0 | 1.11 | 0.13 | 22.4609 | 9.7656 | 2.9297 | 108.74 KB | 61 | | IntelligentCache_Redis | 506.8 us | 10.02 us | 25.85 us | 1,973.1 | 1.26 | 0.15 | 72.2656 | 23.9258 | 3.9063 | 332.72 KB | 62 | | CacheManager_Redis | 2,999.1 us | 52.07 us | 46.16 us | 333.4 | 7.94 | 0.80 | 82.0313 | - | - | 241.57 KB | 63 | 64 | 65 | ## File Caching 66 | 67 | | Method | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Gen 0 | Gen 1 | Allocated | 68 | |----------------------------------------- |---------:|--------:|--------:|--------:|------:|--------:|-------:|-------:|----------:| 69 | | CacheTower_FileCacheLayer_SystemTextJson | 333.9 us | 6.40 us | 8.32 us | 2,994.5 | 0.99 | 0.03 | 0.9766 | 0.4883 | 3 KB | 70 | | CacheTower_FileCacheLayer_Protobuf | 337.1 us | 6.71 us | 7.45 us | 2,966.5 | 0.99 | 0.03 | 0.9766 | 0.4883 | 3 KB | 71 | | CacheTower_FileCacheLayer_NewtonsoftJson | 340.1 us | 3.43 us | 3.21 us | 2,940.5 | 1.00 | 0.00 | 2.9297 | 1.4648 | 9 KB | 72 | | EasyCaching_Disk | 359.7 us | 5.32 us | 4.97 us | 2,780.4 | 1.06 | 0.02 | 1.4648 | - | 5 KB | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TurnerSoftware/CacheTower/4da8de4a637d6a04b0055365a11278619267a412/images/icon.png -------------------------------------------------------------------------------- /src/CacheTower.Extensions.Redis/AssemblyInternals.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("CacheTower.Tests")] -------------------------------------------------------------------------------- /src/CacheTower.Extensions.Redis/CacheTower.Extensions.Redis.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Redis Extensions for Cache Tower 6 | Provides Distributed Locking & Eviction for Cache Tower 7 | redis;$(PackageBaseTags) 8 | James Turner 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/CacheTower.Extensions.Redis/RedisLockExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading.Tasks; 4 | using StackExchange.Redis; 5 | 6 | namespace CacheTower.Extensions.Redis; 7 | 8 | /// 9 | /// Provides distributed cache locking via Redis. 10 | /// 11 | /// 12 | /// Based on
Loris Cro's RedisMemoLock" 13 | /// 14 | public class RedisLockExtension : IDistributedLockExtension 15 | { 16 | private ISubscriber Subscriber { get; } 17 | private IDatabaseAsync Database { get; } 18 | private RedisLockOptions Options { get; } 19 | 20 | private ICacheStack? RegisteredStack { get; set; } 21 | 22 | internal ConcurrentDictionary> LockedOnKeyRefresh { get; } 23 | 24 | /// 25 | /// Creates a new instance of with the given and default lock options. 26 | /// 27 | /// The primary connection to Redis where the distributed lock will be co-ordinated through. 28 | public RedisLockExtension(IConnectionMultiplexer connection) : this(connection, RedisLockOptions.Default) { } 29 | 30 | /// 31 | /// Creates a new instance of with the given and . 32 | /// 33 | /// The primary connection to Redis where the distributed lock will be co-ordinated through. 34 | /// The lock options to configure the behaviour of locking. 35 | public RedisLockExtension(IConnectionMultiplexer connection, RedisLockOptions options) 36 | { 37 | if (connection == null) 38 | { 39 | throw new ArgumentNullException(nameof(connection)); 40 | } 41 | 42 | Options = options; 43 | Database = connection.GetDatabase(options.DatabaseIndex); 44 | Subscriber = connection.GetSubscriber(); 45 | 46 | LockedOnKeyRefresh = new ConcurrentDictionary>(StringComparer.Ordinal); 47 | 48 | Subscriber.Subscribe(GetRedisChannel(), (channel, value) => 49 | { 50 | if (!value.IsNull) 51 | { 52 | UnlockWaitingTasks(value!); 53 | } 54 | }); 55 | } 56 | 57 | private RedisChannel GetRedisChannel() => new(Options.RedisChannel, RedisChannel.PatternMode.Auto); 58 | 59 | /// 60 | public void Register(ICacheStack cacheStack) 61 | { 62 | if (RegisteredStack != null) 63 | { 64 | throw new InvalidOperationException($"{nameof(RedisLockExtension)} can only be registered to one {nameof(ICacheStack)}"); 65 | } 66 | 67 | RegisteredStack = cacheStack; 68 | } 69 | 70 | private async ValueTask ReleaseLockAsync(string cacheKey) 71 | { 72 | var lockKey = string.Format(Options.KeyFormat, cacheKey); 73 | await Subscriber.PublishAsync(GetRedisChannel(), cacheKey, CommandFlags.FireAndForget).ConfigureAwait(false); 74 | await Database.KeyDeleteAsync(lockKey, CommandFlags.FireAndForget).ConfigureAwait(false); 75 | UnlockWaitingTasks(cacheKey); 76 | } 77 | 78 | private async ValueTask SpinWaitAsync(TaskCompletionSource taskCompletionSource, string lockKey) 79 | { 80 | var spinAttempt = 0; 81 | var maxSpinAttempts = Options.LockCheckStrategy.CalculateSpinAttempts(Options.LockTimeout); 82 | while (spinAttempt <= maxSpinAttempts && !taskCompletionSource.Task.IsCanceled && !taskCompletionSource.Task.IsCompleted) 83 | { 84 | spinAttempt++; 85 | 86 | var lockExists = await Database.KeyExistsAsync(lockKey).ConfigureAwait(false); 87 | if (lockExists) 88 | { 89 | await Task.Delay(Options.LockCheckStrategy.SpinTime).ConfigureAwait(false); 90 | continue; 91 | } 92 | 93 | taskCompletionSource.TrySetResult(true); 94 | return; 95 | } 96 | 97 | taskCompletionSource.TrySetCanceled(); 98 | } 99 | 100 | private async ValueTask DelayWaitAsync(TaskCompletionSource taskCompletionSource) 101 | { 102 | await Task.Delay(Options.LockTimeout).ConfigureAwait(false); 103 | taskCompletionSource.TrySetCanceled(); 104 | } 105 | 106 | /// 107 | public async ValueTask AwaitAccessAsync(string cacheKey) 108 | { 109 | var lockKey = string.Format(Options.KeyFormat, cacheKey); 110 | var hasLock = await Database.StringSetAsync(lockKey, RedisValue.EmptyString, expiry: Options.LockTimeout, when: When.NotExists).ConfigureAwait(false); 111 | 112 | if (hasLock) 113 | { 114 | return DistributedLock.Locked(cacheKey, ReleaseLockAsync); 115 | } 116 | else 117 | { 118 | var completionSource = LockedOnKeyRefresh.GetOrAdd(cacheKey, key => 119 | { 120 | var taskCompletionSource = new TaskCompletionSource(); 121 | 122 | if (Options.LockCheckStrategy.UseSpinLock) 123 | { 124 | _ = SpinWaitAsync(taskCompletionSource, lockKey); 125 | } 126 | else 127 | { 128 | _ = DelayWaitAsync(taskCompletionSource); 129 | } 130 | 131 | return taskCompletionSource; 132 | }); 133 | 134 | //Last minute check to confirm whether waiting is required (in case the notification is missed) 135 | if (!await Database.KeyExistsAsync(lockKey).ConfigureAwait(false)) 136 | { 137 | UnlockWaitingTasks(cacheKey); 138 | return DistributedLock.Unlocked(cacheKey); 139 | } 140 | 141 | await completionSource.Task.ConfigureAwait(false); 142 | return DistributedLock.Unlocked(cacheKey); 143 | } 144 | } 145 | 146 | private void UnlockWaitingTasks(string cacheKey) 147 | { 148 | if (LockedOnKeyRefresh.TryRemove(cacheKey, out var waitingTasks)) 149 | { 150 | waitingTasks.TrySetResult(true); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/CacheTower.Extensions.Redis/RedisLockOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CacheTower.Extensions.Redis; 4 | 5 | /// 6 | /// Lock options for use by the . 7 | /// 8 | /// How long to wait on the lock before having it expire. 9 | /// The Redis channel to communicate unlocking events across. 10 | /// 11 | /// A compatible string used to create the lock key stored in Redis. 12 | /// The cache key is provided as argument {0}. 13 | /// 14 | /// 15 | /// The database index used for the Redis lock. 16 | /// If not specified, uses the default database as configured on the connection. 17 | /// 18 | /// 19 | /// The lock checking strategy to use, like pub/sub or spin lock, to detect lock release locally. 20 | /// The waiter on the lock also performs a tight loop to check for lock release. 21 | /// This can avoid the situation of a missed message on Redis pub/sub. 22 | /// 23 | public record struct RedisLockOptions( 24 | TimeSpan LockTimeout, 25 | string RedisChannel, 26 | string KeyFormat, 27 | int DatabaseIndex, 28 | LockCheckStrategy LockCheckStrategy 29 | ) 30 | { 31 | /// 32 | /// The default options for . 33 | /// 34 | /// 35 | /// 36 | /// - : 1 minute
37 | /// - : "CacheTower.CacheLock"
38 | /// - : "Lock:{0}"
39 | /// - : The default database configured on the connection.
40 | /// - : Use Redis pub/sub notification to determine end of lock. 41 | ///
42 | ///
43 | public static readonly RedisLockOptions Default = new( 44 | LockTimeout: TimeSpan.FromMinutes(1), 45 | RedisChannel: "CacheTower.CacheLock", 46 | KeyFormat: "Lock:{0}", 47 | DatabaseIndex: -1, 48 | LockCheckStrategy: LockCheckStrategy.WithPubSubNotification() 49 | ); 50 | } 51 | 52 | /// 53 | /// The lock checking strategy to use for the . 54 | /// 55 | public readonly struct LockCheckStrategy 56 | { 57 | /// 58 | /// Whether a "spin lock" strategy will be used. 59 | /// 60 | public readonly bool UseSpinLock { get; private init; } 61 | /// 62 | /// For spin lock strategies, the time to wait between lock checks. 63 | /// 64 | public readonly TimeSpan SpinTime { get; private init; } 65 | 66 | /// 67 | /// Use a Redis pub/sub notification lock checking strategy. 68 | /// 69 | /// 70 | public static LockCheckStrategy WithPubSubNotification() => new(); 71 | 72 | /// 73 | /// Use a "spin lock" lock checking strategy. 74 | /// This can avoid the situation of a missed message on Redis pub/sub. 75 | /// 76 | /// The time to wait between lock checks. 77 | /// 78 | public static LockCheckStrategy WithSpinLock(TimeSpan spinTime) 79 | { 80 | return new LockCheckStrategy 81 | { 82 | UseSpinLock = true, 83 | SpinTime = spinTime 84 | }; 85 | } 86 | 87 | internal int CalculateSpinAttempts(TimeSpan lockTimeout) 88 | { 89 | if (!UseSpinLock) 90 | { 91 | return 0; 92 | } 93 | 94 | return (int)Math.Ceiling(lockTimeout.TotalMilliseconds / SpinTime.TotalMilliseconds); 95 | } 96 | } -------------------------------------------------------------------------------- /src/CacheTower.Extensions.Redis/RedisRemoteEvictionExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using StackExchange.Redis; 5 | 6 | namespace CacheTower.Extensions.Redis 7 | { 8 | /// 9 | /// The broadcasts cache updates, evictions and flushes to Redis to allow for remote eviction of old cache data. 10 | /// When one of these events is received, it will perform that action locally to the configured cache layers. 11 | /// 12 | public class RedisRemoteEvictionExtension : ICacheChangeExtension 13 | { 14 | private ISubscriber Subscriber { get; } 15 | private RedisChannel FlushChannel { get; } 16 | private RedisChannel EvictionChannel { get; } 17 | 18 | private bool IsRegistered { get; set; } 19 | 20 | private readonly object LockObj = new object(); 21 | private HashSet FlaggedEvictions { get; } 22 | private bool HasFlushTriggered { get; set; } 23 | 24 | /// 25 | /// Creates a new instance of . 26 | /// 27 | /// The primary connection to the Redis instance where the messages will be broadcast and received through. 28 | /// The channel prefix to use for the Redis communication. 29 | public RedisRemoteEvictionExtension(IConnectionMultiplexer connection, string channelPrefix = "CacheTower") 30 | { 31 | if (connection == null) 32 | { 33 | throw new ArgumentNullException(nameof(connection)); 34 | } 35 | 36 | if (channelPrefix == null) 37 | { 38 | throw new ArgumentNullException(nameof(channelPrefix)); 39 | } 40 | 41 | Subscriber = connection.GetSubscriber(); 42 | FlushChannel = new($"{channelPrefix}.RemoteFlush", RedisChannel.PatternMode.Auto); 43 | EvictionChannel = new($"{channelPrefix}.RemoteEviction", RedisChannel.PatternMode.Auto); 44 | FlaggedEvictions = new HashSet(StringComparer.Ordinal); 45 | } 46 | 47 | /// 48 | /// This will broadcast to Redis that the cache entry belonging to is now out-of-date and should be evicted. 49 | /// 50 | /// 51 | public ValueTask OnCacheUpdateAsync(string cacheKey, DateTime expiry, CacheUpdateType cacheUpdateType) 52 | { 53 | if (cacheUpdateType == CacheUpdateType.AddOrUpdateEntry) 54 | { 55 | return FlagEvictionAsync(cacheKey); 56 | } 57 | return default; 58 | } 59 | /// 60 | /// This will broadcast to Redis that the cache entry belonging to is to be evicted. 61 | /// 62 | /// 63 | public ValueTask OnCacheEvictionAsync(string cacheKey) 64 | { 65 | return FlagEvictionAsync(cacheKey); 66 | } 67 | 68 | private async ValueTask FlagEvictionAsync(string cacheKey) 69 | { 70 | lock (LockObj) 71 | { 72 | FlaggedEvictions.Add(cacheKey); 73 | } 74 | 75 | await Subscriber.PublishAsync(EvictionChannel, cacheKey, CommandFlags.FireAndForget).ConfigureAwait(false); 76 | } 77 | 78 | /// 79 | /// This will broadcast to Redis that the cache should be flushed. 80 | /// 81 | /// 82 | public async ValueTask OnCacheFlushAsync() 83 | { 84 | lock (LockObj) 85 | { 86 | HasFlushTriggered = true; 87 | } 88 | 89 | await Subscriber.PublishAsync(FlushChannel, RedisValue.EmptyString, CommandFlags.FireAndForget).ConfigureAwait(false); 90 | } 91 | 92 | /// 93 | public void Register(ICacheStack cacheStack) 94 | { 95 | if (IsRegistered) 96 | { 97 | throw new InvalidOperationException($"{nameof(RedisRemoteEvictionExtension)} can only be registered to one {nameof(ICacheStack)}"); 98 | } 99 | IsRegistered = true; 100 | 101 | Subscriber.Subscribe(EvictionChannel) 102 | .OnMessage(async (channelMessage) => 103 | { 104 | if (channelMessage.Message.IsNull) 105 | { 106 | return; 107 | } 108 | 109 | string cacheKey = channelMessage.Message!; 110 | 111 | var shouldEvictLocally = false; 112 | lock (LockObj) 113 | { 114 | shouldEvictLocally = FlaggedEvictions.Remove(cacheKey) == false; 115 | } 116 | 117 | if (shouldEvictLocally) 118 | { 119 | var cacheLayers = ((IExtendableCacheStack)cacheStack).GetCacheLayers(); 120 | for (var i = 0; i < cacheLayers.Count; i++) 121 | { 122 | var cacheLayer = cacheLayers[i]; 123 | if (cacheLayer is ILocalCacheLayer) 124 | { 125 | await cacheLayer.EvictAsync(cacheKey).ConfigureAwait(false); 126 | } 127 | } 128 | } 129 | }); 130 | 131 | Subscriber.Subscribe(FlushChannel) 132 | .OnMessage(async (channelMessage) => 133 | { 134 | var shouldFlushLocally = false; 135 | lock (LockObj) 136 | { 137 | shouldFlushLocally = !HasFlushTriggered; 138 | HasFlushTriggered = false; 139 | } 140 | 141 | if (shouldFlushLocally) 142 | { 143 | var cacheLayers = ((IExtendableCacheStack)cacheStack).GetCacheLayers(); 144 | for (var i = 0; i < cacheLayers.Count; i++) 145 | { 146 | var cacheLayer = cacheLayers[i]; 147 | if (cacheLayer is ILocalCacheLayer) 148 | { 149 | await cacheLayer.FlushAsync().ConfigureAwait(false); 150 | } 151 | } 152 | } 153 | }); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/CacheTower.Extensions.Redis/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using CacheTower; 2 | using CacheTower.Extensions.Redis; 3 | using StackExchange.Redis; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection; 6 | 7 | /// 8 | /// Microsoft extensions for Cache Tower. 9 | /// 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// 13 | /// Adds the to the with the specified and . 14 | /// 15 | /// 16 | /// The connection to the Redis server. 17 | /// 18 | public static ICacheStackBuilder WithRedisDistributedLocking(this ICacheStackBuilder builder, IConnectionMultiplexer connection) 19 | { 20 | return builder.WithRedisDistributedLocking(connection, RedisLockOptions.Default); 21 | } 22 | 23 | /// 24 | /// Adds the to the with the specified and . 25 | /// 26 | /// 27 | /// The connection to the Redis server. 28 | /// Options to configure the Redis distributed locking extension. 29 | /// 30 | public static ICacheStackBuilder WithRedisDistributedLocking(this ICacheStackBuilder builder, IConnectionMultiplexer connection, RedisLockOptions options) 31 | { 32 | builder.Extensions.Add(new RedisLockExtension(connection, options)); 33 | return builder; 34 | } 35 | 36 | /// 37 | /// Adds the to the with the specified and . 38 | /// 39 | /// 40 | /// The extension will only evict from cache layers in the that implement . 41 | /// 42 | /// 43 | /// The connection to the Redis server. 44 | /// The channel prefix to use for the pub/sub calls to other instances. 45 | /// 46 | public static ICacheStackBuilder WithRedisRemoteEviction(this ICacheStackBuilder builder, IConnectionMultiplexer connection, string channelPrefix = "CacheTower") 47 | { 48 | builder.Extensions.Add(new RedisRemoteEvictionExtension(connection, channelPrefix)); 49 | return builder; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Database.MongoDB/CacheTower.Providers.Database.MongoDB.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | MongoDB Provider for Cache Tower 6 | Use MongoDB for caching with Cache Tower 7 | mongodb;$(PackageBaseTags) 8 | James Turner 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | <_Parameter1>false 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Database.MongoDB/Commands/CleanupCommand.cs: -------------------------------------------------------------------------------- 1 | using CacheTower.Providers.Database.MongoDB.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using MongoDB.Driver; 5 | using MongoFramework.Infrastructure.Commands; 6 | 7 | namespace CacheTower.Providers.Database.MongoDB.Commands 8 | { 9 | internal class CleanupCommand : IWriteCommand 10 | { 11 | public Type EntityType => typeof(DbCachedEntry); 12 | 13 | public IEnumerable> GetModel(WriteModelOptions options) 14 | { 15 | var filter = Builders.Filter.Lt(e => e.Expiry, DateTime.UtcNow); 16 | yield return new DeleteManyModel(filter); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Database.MongoDB/Commands/EvictCommand.cs: -------------------------------------------------------------------------------- 1 | using CacheTower.Providers.Database.MongoDB.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using MongoDB.Driver; 5 | using MongoFramework.Infrastructure.Commands; 6 | 7 | namespace CacheTower.Providers.Database.MongoDB.Commands 8 | { 9 | internal class EvictCommand : IWriteCommand 10 | { 11 | private string CacheKey { get; } 12 | 13 | public Type EntityType => typeof(DbCachedEntry); 14 | 15 | public EvictCommand(string cacheKey) 16 | { 17 | CacheKey = cacheKey; 18 | } 19 | 20 | public IEnumerable> GetModel(WriteModelOptions options) 21 | { 22 | var filter = Builders.Filter.Eq(e => e.CacheKey, CacheKey); 23 | yield return new DeleteManyModel(filter); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Database.MongoDB/Commands/FlushCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CacheTower.Providers.Database.MongoDB.Entities; 4 | using MongoDB.Driver; 5 | using MongoFramework.Infrastructure.Commands; 6 | 7 | namespace CacheTower.Providers.Database.MongoDB.Commands 8 | { 9 | internal class FlushCommand : IWriteCommand 10 | { 11 | public Type EntityType => typeof(DbCachedEntry); 12 | 13 | public IEnumerable> GetModel(WriteModelOptions options) 14 | { 15 | var filter = Builders.Filter.Empty; 16 | yield return new DeleteManyModel(filter); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Database.MongoDB/Commands/SetCommand.cs: -------------------------------------------------------------------------------- 1 | using CacheTower.Providers.Database.MongoDB.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using MongoDB.Driver; 5 | using MongoFramework.Infrastructure.Commands; 6 | 7 | namespace CacheTower.Providers.Database.MongoDB.Commands 8 | { 9 | internal class SetCommand : IWriteCommand 10 | { 11 | public DbCachedEntry Entry { get; } 12 | 13 | public Type EntityType => typeof(DbCachedEntry); 14 | 15 | public SetCommand(DbCachedEntry dbCachedEntry) 16 | { 17 | Entry = dbCachedEntry; 18 | } 19 | 20 | public IEnumerable> GetModel(WriteModelOptions options) 21 | { 22 | var filter = Builders.Filter.Eq(e => e.CacheKey, Entry.CacheKey); 23 | var updateDefinition = Builders.Update 24 | .Set(e => e.CacheKey, Entry.CacheKey) 25 | .Set(e => e.Expiry, Entry.Expiry) 26 | .Set(e => e.Value, Entry.Value); 27 | 28 | var model = new UpdateOneModel(filter, updateDefinition) 29 | { 30 | IsUpsert = true 31 | }; 32 | 33 | yield return model; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Database.MongoDB/Entities/DbCachedEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using MongoDB.Bson; 5 | using MongoDB.Bson.Serialization.Attributes; 6 | using MongoDB.Bson.Serialization.IdGenerators; 7 | using MongoFramework.Attributes; 8 | 9 | namespace CacheTower.Providers.Database.MongoDB.Entities 10 | { 11 | internal class DbCachedEntry 12 | { 13 | public ObjectId Id { get; set; } 14 | 15 | [Index(MongoFramework.IndexSortOrder.Ascending)] 16 | public string? CacheKey { get; set; } 17 | 18 | [Index(MongoFramework.IndexSortOrder.Ascending)] 19 | public DateTime Expiry { get; set; } 20 | 21 | public object? Value { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Database.MongoDB/MongoDbCacheLayer.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using CacheTower.Providers.Database.MongoDB.Commands; 4 | using CacheTower.Providers.Database.MongoDB.Entities; 5 | using MongoFramework; 6 | using MongoFramework.Infrastructure; 7 | using MongoFramework.Infrastructure.Indexing; 8 | using MongoFramework.Infrastructure.Linq; 9 | using MongoFramework.Infrastructure.Mapping; 10 | 11 | namespace CacheTower.Providers.Database.MongoDB 12 | { 13 | /// 14 | /// The allows caching through a MongoDB server. 15 | /// Cache entries are serialized to BSON using . 16 | /// 17 | /// 18 | public class MongoDbCacheLayer : IDistributedCacheLayer 19 | { 20 | private bool? IsDatabaseAvailable { get; set; } 21 | 22 | private IMongoDbConnection Connection { get; } 23 | 24 | private bool HasSetIndexes = false; 25 | 26 | static MongoDbCacheLayer() 27 | { 28 | //Due to a change in 2.19.0, we need to ensure that DbCachedEntry is registered early. 29 | //More importantly than the type itself is that the TypeDiscoverySerializer is registered 30 | //which is done automatically with use of type discovery serialization. 31 | //This may need to be revisited later with a future update to MongoFramework. 32 | _ = EntityMapping.RegisterType(typeof(DbCachedEntry)); 33 | } 34 | 35 | /// 36 | /// Creates a new instance of with the given . 37 | /// 38 | /// The connection to the MongoDB database. 39 | public MongoDbCacheLayer(IMongoDbConnection connection) 40 | { 41 | Connection = connection; 42 | } 43 | 44 | private async ValueTask TryConfigureIndexes() 45 | { 46 | if (!HasSetIndexes) 47 | { 48 | HasSetIndexes = true; 49 | await EntityIndexWriter.ApplyIndexingAsync(Connection).ConfigureAwait(false); 50 | } 51 | } 52 | 53 | /// 54 | public async ValueTask CleanupAsync() 55 | { 56 | await TryConfigureIndexes().ConfigureAwait(false); 57 | await EntityCommandWriter.WriteAsync(Connection, new[] { new CleanupCommand() }, default).ConfigureAwait(false); 58 | } 59 | 60 | /// 61 | public async ValueTask EvictAsync(string cacheKey) 62 | { 63 | await TryConfigureIndexes().ConfigureAwait(false); 64 | await EntityCommandWriter.WriteAsync(Connection, new[] { new EvictCommand(cacheKey) }, default).ConfigureAwait(false); 65 | } 66 | 67 | /// 68 | public async ValueTask FlushAsync() 69 | { 70 | await EntityCommandWriter.WriteAsync(Connection, new[] { new FlushCommand() }, default).ConfigureAwait(false); 71 | } 72 | 73 | /// 74 | public async ValueTask?> GetAsync(string cacheKey) 75 | { 76 | await TryConfigureIndexes().ConfigureAwait(false); 77 | 78 | var provider = new MongoFrameworkQueryProvider(Connection); 79 | var queryable = new MongoFrameworkQueryable(provider); 80 | 81 | var dbEntry = queryable.Where(e => e.CacheKey == cacheKey).FirstOrDefault(); 82 | var cacheEntry = default(CacheEntry); 83 | 84 | if (dbEntry != default) 85 | { 86 | cacheEntry = new CacheEntry((T)dbEntry.Value!, dbEntry.Expiry); 87 | } 88 | 89 | return cacheEntry; 90 | } 91 | 92 | /// 93 | public async ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry) 94 | { 95 | await TryConfigureIndexes().ConfigureAwait(false); 96 | var command = new SetCommand(new DbCachedEntry 97 | { 98 | CacheKey = cacheKey, 99 | Expiry = cacheEntry.Expiry, 100 | Value = cacheEntry.Value! 101 | }); 102 | 103 | await EntityCommandWriter.WriteAsync(Connection, new[] { command }, default).ConfigureAwait(false); 104 | } 105 | 106 | /// 107 | public async ValueTask IsAvailableAsync(string cacheKey) 108 | { 109 | if (IsDatabaseAvailable == null) 110 | { 111 | try 112 | { 113 | await TryConfigureIndexes().ConfigureAwait(false); 114 | IsDatabaseAvailable = true; 115 | } 116 | catch 117 | { 118 | IsDatabaseAvailable = false; 119 | } 120 | } 121 | 122 | return IsDatabaseAvailable.Value; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Database.MongoDB/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using CacheTower; 2 | using CacheTower.Providers.Database.MongoDB; 3 | using MongoFramework; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection; 6 | 7 | /// 8 | /// Microsoft extensions for Cache Tower. 9 | /// 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// 13 | /// Adds a to the with the specified . 14 | /// 15 | /// 16 | /// The connection to the MongoDB server. 17 | /// 18 | public static ICacheStackBuilder AddMongoDbCacheLayer(this ICacheStackBuilder builder, IMongoDbConnection connection) 19 | { 20 | builder.CacheLayers.Add(new MongoDbCacheLayer(connection)); 21 | return builder; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.FileSystem.Json/CacheTower.Providers.FileSystem.Json.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | JSON File Provider for Cache Tower 6 | Use JSON serialized files for caching with Cache Tower 7 | json;filesystem;$(PackageBaseTags) 8 | James Turner 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.FileSystem.Json/JsonFileCacheLayer.cs: -------------------------------------------------------------------------------- 1 | using CacheTower.Serializers.NewtonsoftJson; 2 | using System; 3 | 4 | namespace CacheTower.Providers.FileSystem.Json 5 | { 6 | /// 7 | /// The uses Newtonsoft.Json to serialize and deserialize the cache items to the file system. 8 | /// 9 | /// 10 | [Obsolete("Use FileCacheLayer and specify the NewtonsoftJsonCacheSerializer. This cache layer (and the associated package) will be discontinued in a future release.")] 11 | public class JsonFileCacheLayer : FileCacheLayer, ICacheLayer 12 | { 13 | /// 14 | /// Creates a , using the given as the location to store the cache. 15 | /// 16 | /// 17 | public JsonFileCacheLayer(string directoryPath) : base(new FileCacheLayerOptions(directoryPath, NewtonsoftJsonCacheSerializer.Instance)) { } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.FileSystem.Protobuf/CacheTower.Providers.FileSystem.Protobuf.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Protobuf File Provider for Cache Tower 6 | Use Protobuf serialized files for caching with Cache Tower 7 | protobuf;filesystem;$(PackageBaseTags) 8 | James Turner 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.FileSystem.Protobuf/ProtobufFileCacheLayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CacheTower.Serializers.Protobuf; 3 | 4 | namespace CacheTower.Providers.FileSystem.Protobuf 5 | { 6 | /// 7 | /// The uses protobuf-net to serialize and deserialize the cache items to the file system. 8 | /// 9 | /// When caching custom types, you will need to decorate your class with [ProtoContact] and [ProtoMember] attributes per protobuf-net's documentation. 10 | /// While this can be inconvienent, using protobuf-net ensures high performance and low allocations for serializing. 11 | /// 12 | /// 13 | /// 14 | [Obsolete("Use FileCacheLayer directly and specify the ProtobufCacheSerializer. This cache layer (and the associated package) will be discontinued in a future release.")] 15 | public class ProtobufFileCacheLayer : FileCacheLayer 16 | { 17 | /// 18 | /// Creates a , using the given as the location to store the cache. 19 | /// 20 | /// 21 | public ProtobufFileCacheLayer(string directoryPath) : base(new FileCacheLayerOptions(directoryPath, ProtobufCacheSerializer.Instance)) { } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Redis/CacheTower.Providers.Redis.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Redis Provider for Cache Tower 6 | Use Redis for caching with Cache Tower 7 | redis;$(PackageBaseTags) 8 | James Turner 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Redis/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace System.Runtime.CompilerServices 4 | { 5 | /// 6 | /// Reserved to be used by the compiler for tracking metadata. 7 | /// This class should not be used by developers in source code. 8 | /// 9 | [EditorBrowsable(EditorBrowsableState.Never)] 10 | internal static class IsExternalInit 11 | { 12 | } 13 | } -------------------------------------------------------------------------------- /src/CacheTower.Providers.Redis/RedisCacheLayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using CacheTower.Serializers.Protobuf; 5 | using StackExchange.Redis; 6 | 7 | namespace CacheTower.Providers.Redis 8 | { 9 | /// 10 | public class RedisCacheLayer : IDistributedCacheLayer 11 | { 12 | private IConnectionMultiplexer Connection { get; } 13 | private IDatabaseAsync Database { get; } 14 | private readonly RedisCacheLayerOptions Options; 15 | 16 | /// 17 | /// Creates a new instance of with the given and . 18 | /// If using this constructor, Protobuf encoding will be used. 19 | /// 20 | /// The primary connection to Redis where the cache will be stored. 21 | /// 22 | /// The database index to use for Redis. 23 | /// If not specified, uses the default database as configured on the . 24 | /// 25 | [Obsolete("Use other constructor. Specifying cache serializers will become the default behaviour going forward.")] 26 | public RedisCacheLayer(IConnectionMultiplexer connection, int databaseIndex = -1) : this(connection, new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance, databaseIndex)) 27 | { 28 | } 29 | 30 | /// 31 | /// Creates a new instance of with the given and . 32 | /// 33 | /// The primary connection to Redis where the cache will be stored. 34 | /// Various options that control the behaviour of the . 35 | public RedisCacheLayer(IConnectionMultiplexer connection, RedisCacheLayerOptions options) 36 | { 37 | Connection = connection; 38 | Database = connection.GetDatabase(options.DatabaseIndex); 39 | Options = options; 40 | } 41 | 42 | /// 43 | /// 44 | /// Cleanup is unnecessary for the as Redis handles removing expired keys automatically. 45 | /// 46 | public ValueTask CleanupAsync() 47 | { 48 | //Noop as Redis handles this directly 49 | return new ValueTask(); 50 | } 51 | 52 | /// 53 | public async ValueTask EvictAsync(string cacheKey) 54 | { 55 | await Database.KeyDeleteAsync(cacheKey).ConfigureAwait(false); 56 | } 57 | 58 | /// 59 | /// 60 | /// Flushing the performs a database flush in Redis. 61 | /// Every key associated to the database index will be removed. 62 | /// 63 | public async ValueTask FlushAsync() 64 | { 65 | var redisEndpoints = Connection.GetEndPoints(); 66 | foreach (var endpoint in redisEndpoints) 67 | { 68 | await Connection.GetServer(endpoint).FlushDatabaseAsync(Options.DatabaseIndex).ConfigureAwait(false); 69 | } 70 | } 71 | 72 | /// 73 | public async ValueTask?> GetAsync(string cacheKey) 74 | { 75 | var redisValue = await Database.StringGetAsync(cacheKey).ConfigureAwait(false); 76 | if (redisValue != RedisValue.Null) 77 | { 78 | using var stream = new MemoryStream(redisValue); 79 | return Options.Serializer.Deserialize>(stream); 80 | } 81 | 82 | return default; 83 | } 84 | 85 | /// 86 | public ValueTask IsAvailableAsync(string cacheKey) 87 | { 88 | return new ValueTask(Connection.IsConnected); 89 | } 90 | 91 | /// 92 | public async ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry) 93 | { 94 | var expiryOffset = cacheEntry.Expiry - DateTime.UtcNow; 95 | if (expiryOffset < TimeSpan.Zero) 96 | { 97 | return; 98 | } 99 | 100 | using var stream = new MemoryStream(); 101 | Options.Serializer.Serialize(stream, cacheEntry); 102 | stream.Seek(0, SeekOrigin.Begin); 103 | var redisValue = RedisValue.CreateFrom(stream); 104 | await Database.StringSetAsync(cacheKey, redisValue, expiryOffset).ConfigureAwait(false); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Redis/RedisCacheLayerOptions.cs: -------------------------------------------------------------------------------- 1 | namespace CacheTower.Providers.Redis; 2 | 3 | /// 4 | /// Options for controlling a . 5 | /// 6 | /// The serializer to use for the data. 7 | /// 8 | /// The database index used for the cached data. 9 | /// If none is specified, uses the default database as configured on the connection. 10 | /// 11 | public readonly record struct RedisCacheLayerOptions( 12 | ICacheSerializer Serializer, 13 | int DatabaseIndex = -1 14 | ); 15 | -------------------------------------------------------------------------------- /src/CacheTower.Providers.Redis/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using CacheTower; 2 | using CacheTower.Providers.Redis; 3 | using StackExchange.Redis; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection; 6 | 7 | /// 8 | /// Microsoft extensions for Cache Tower. 9 | /// 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// 13 | /// Adds a to the with the specified and . 14 | /// 15 | /// 16 | /// The connection to the MongoDB server. 17 | /// The options for configuring serializer and database index. 18 | /// 19 | public static ICacheStackBuilder AddRedisCacheLayer(this ICacheStackBuilder builder, IConnectionMultiplexer connection, RedisCacheLayerOptions options) 20 | { 21 | builder.CacheLayers.Add(new RedisCacheLayer(connection, options)); 22 | return builder; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/CacheTower.Serializers.NewtonsoftJson/CacheTower.Serializers.NewtonsoftJson.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Newtonsoft.Json Serializer for Cache Tower 6 | Newtonsoft.Json cache serialization for Cache Tower 7 | newtonsoft;json;$(PackageBaseTags) 8 | James Turner 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/CacheTower.Serializers.NewtonsoftJson/NewtonsoftJsonCacheSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using Newtonsoft.Json; 5 | 6 | namespace CacheTower.Serializers.NewtonsoftJson; 7 | 8 | /// 9 | /// Allows serializing to and from JSON via Newtonsoft.Json 10 | /// 11 | public class NewtonsoftJsonCacheSerializer : ICacheSerializer 12 | { 13 | private readonly JsonSerializer serializer; 14 | 15 | /// 16 | /// An existing instance of . 17 | /// 18 | public static NewtonsoftJsonCacheSerializer Instance { get; } = new(); 19 | 20 | private NewtonsoftJsonCacheSerializer() 21 | { 22 | serializer = new JsonSerializer(); 23 | } 24 | 25 | /// 26 | /// Creates a new instance of with the specified . 27 | /// 28 | public NewtonsoftJsonCacheSerializer(JsonSerializerSettings settings) 29 | { 30 | serializer = JsonSerializer.Create(settings); 31 | } 32 | 33 | /// 34 | public void Serialize(Stream stream, T? value) 35 | { 36 | try 37 | { 38 | using var streamWriter = new StreamWriter(stream, Encoding.UTF8, 1024, true); 39 | using var jsonWriter = new JsonTextWriter(streamWriter); 40 | serializer.Serialize(jsonWriter, value); 41 | } 42 | catch (Exception ex) 43 | { 44 | throw new CacheSerializationException("A serialization error has occurred when serializing with Newtonsoft.Json", ex); 45 | } 46 | } 47 | 48 | /// 49 | public T? Deserialize(Stream stream) 50 | { 51 | try 52 | { 53 | using var streamReader = new StreamReader(stream, Encoding.UTF8, false, 1024); 54 | using var jsonReader = new JsonTextReader(streamReader); 55 | return serializer.Deserialize(jsonReader); 56 | } 57 | catch (Exception ex) 58 | { 59 | throw new CacheSerializationException("A serialization error has occurred when deserializing with Newtonsoft.Json", ex); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/CacheTower.Serializers.Protobuf/CacheTower.Serializers.Protobuf.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Protobuf Serializer for Cache Tower 6 | Protobuf cache serialization for Cache Tower 7 | protobuf;$(PackageBaseTags) 8 | James Turner 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/CacheTower.Serializers.Protobuf/ProtobufCacheSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using CacheTower.Providers.FileSystem; 4 | using ProtoBuf; 5 | using ProtoBuf.Meta; 6 | 7 | namespace CacheTower.Serializers.Protobuf 8 | { 9 | /// 10 | /// Allows serializing to and from Protobuf format via protobuf-net. 11 | /// 12 | /// 13 | /// 14 | /// When caching custom types, you will need to decorate your class with [ProtoContact] and [ProtoMember] attributes per protobuf-net's documentation.
15 | /// Additionally, as the Protobuf format doesn't have a way to represent an empty collection, these will be returned as null. 16 | ///
17 | ///
18 | public class ProtobufCacheSerializer : ICacheSerializer 19 | { 20 | static ProtobufCacheSerializer() 21 | { 22 | RuntimeTypeModel.Default.Add(applyDefaultBehaviour: false) 23 | .Add(1, nameof(ManifestEntry.FileName)) 24 | .Add(2, nameof(ManifestEntry.Expiry)); 25 | } 26 | 27 | /// 28 | /// An existing instance of . 29 | /// 30 | public static ProtobufCacheSerializer Instance { get; } = new(); 31 | 32 | //Because we can't use an open generic for protobuf-net (see https://github.com/protobuf-net/protobuf-net/issues/802) 33 | //we instead use/abuse a static class with a generic parameter to dynamically create the serialization config for us. 34 | private static class SerializerConfig 35 | { 36 | static SerializerConfig() 37 | { 38 | if (typeof(ICacheEntry).IsAssignableFrom(typeof(T))) 39 | { 40 | RuntimeTypeModel.Default.Add(typeof(T), applyDefaultBehaviour: false) 41 | .Add(1, nameof(CacheEntry.Expiry)) 42 | .Add(2, nameof(CacheEntry.Value)); 43 | } 44 | } 45 | 46 | //This method is only here to make the action more explicit 47 | public static void EnsureConfigured() { } 48 | } 49 | 50 | /// 51 | public void Serialize(Stream stream, T? value) 52 | { 53 | try 54 | { 55 | SerializerConfig.EnsureConfigured(); 56 | Serializer.Serialize(stream, value); 57 | } 58 | catch (Exception ex) 59 | { 60 | throw new CacheSerializationException("A serialization error has occurred when serializing with ProtoBuf", ex); 61 | } 62 | } 63 | 64 | /// 65 | public T? Deserialize(Stream stream) 66 | { 67 | try 68 | { 69 | SerializerConfig.EnsureConfigured(); 70 | return Serializer.Deserialize(stream); 71 | } 72 | catch (Exception ex) 73 | { 74 | throw new CacheSerializationException("A serialization error has occurred when deserializing with ProtoBuf", ex); 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/CacheTower.Serializers.SystemTextJson/CacheTower.Serializers.SystemTextJson.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | System.Text.Json Serializer for Cache Tower 6 | System.Text.Json cache serialization for Cache Tower 7 | system.text.json;json;$(PackageBaseTags) 8 | James Turner 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/CacheTower.Serializers.SystemTextJson/SystemTextJsonCacheSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text.Json; 4 | 5 | namespace CacheTower.Serializers.SystemTextJson; 6 | 7 | /// 8 | /// Allows serializing to and from JSON via System.Text.Json 9 | /// 10 | public class SystemTextJsonCacheSerializer : ICacheSerializer 11 | { 12 | private readonly JsonSerializerOptions? options; 13 | 14 | /// 15 | /// An existing instance of . 16 | /// 17 | public static SystemTextJsonCacheSerializer Instance { get; } = new(); 18 | 19 | private SystemTextJsonCacheSerializer() { } 20 | 21 | /// 22 | /// Creates a new instance of with the specified . 23 | /// 24 | public SystemTextJsonCacheSerializer(JsonSerializerOptions options) 25 | { 26 | this.options = options; 27 | } 28 | 29 | /// 30 | public void Serialize(Stream stream, T? value) 31 | { 32 | try 33 | { 34 | JsonSerializer.Serialize(stream, value, options); 35 | } 36 | catch (Exception ex) 37 | { 38 | throw new CacheSerializationException("A serialization error has occurred when serializing with System.Text.Json", ex); 39 | } 40 | } 41 | 42 | /// 43 | public T? Deserialize(Stream stream) 44 | { 45 | try 46 | { 47 | return JsonSerializer.Deserialize(stream, options); 48 | } 49 | catch (Exception ex) 50 | { 51 | throw new CacheSerializationException("A serialization error has occurred when deserializing with System.Text.Json", ex); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/CacheTower/AssemblyInternals.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("CacheTower.Tests")] 4 | [assembly: InternalsVisibleTo("CacheTower.Benchmarks")] 5 | [assembly: InternalsVisibleTo("CacheTower.Extensions.Redis")] -------------------------------------------------------------------------------- /src/CacheTower/CacheContextActivators.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace CacheTower; 5 | 6 | internal readonly struct ServiceProviderContextActivator : ICacheContextActivator 7 | { 8 | private readonly IServiceProvider ServiceProvider; 9 | 10 | public ServiceProviderContextActivator(IServiceProvider serviceProvider) 11 | { 12 | ServiceProvider = serviceProvider; 13 | } 14 | 15 | public ICacheContextScope BeginScope() 16 | { 17 | return new ServiceProviderContextScope(ServiceProvider.CreateScope()); 18 | } 19 | } 20 | 21 | internal readonly struct ServiceProviderContextScope : ICacheContextScope 22 | { 23 | private readonly IServiceScope ServiceScope; 24 | 25 | public ServiceProviderContextScope(IServiceScope serviceScope) 26 | { 27 | ServiceScope = serviceScope; 28 | } 29 | 30 | public object Resolve(Type type) 31 | { 32 | return ServiceScope.ServiceProvider.GetRequiredService(type); 33 | } 34 | 35 | public void Dispose() 36 | { 37 | ServiceScope.Dispose(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/CacheTower/CacheEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using CacheTower.Internal; 4 | 5 | namespace CacheTower; 6 | 7 | /// 8 | /// Container for the cache entry expiry date. 9 | /// 10 | public interface ICacheEntry 11 | { 12 | /// 13 | /// The expiry date for the cache entry. 14 | /// 15 | DateTime Expiry { get; } 16 | } 17 | 18 | /// 19 | /// Container for the cached value and its expiry date. 20 | /// 21 | /// 22 | public interface ICacheEntry : ICacheEntry 23 | { 24 | /// 25 | /// The cached value. 26 | /// 27 | T? Value { get; } 28 | } 29 | 30 | /// 31 | /// Extension methods for . 32 | /// 33 | public static class CacheEntryExtensions 34 | { 35 | /// 36 | /// Calculates the stale date for an based on the 's expiry and . 37 | /// 38 | /// 39 | /// When is , this will return the 's expiry. 40 | /// 41 | /// The cache entry to get the stale date for. 42 | /// The cache settings to use as part of the stale date calculation. 43 | /// 44 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 45 | public static DateTime GetStaleDate(this ICacheEntry cacheEntry, CacheSettings cacheSettings) 46 | { 47 | if (cacheSettings.StaleAfter.HasValue) 48 | { 49 | return cacheEntry.Expiry - cacheSettings.TimeToLive + cacheSettings.StaleAfter!.Value; 50 | } 51 | else 52 | { 53 | return cacheEntry.Expiry; 54 | } 55 | } 56 | } 57 | 58 | /// 59 | /// Container for both the cached value and its expiry date. 60 | /// 61 | /// 62 | /// The value to cache. 63 | /// The expiry date of the cache entry. This will be rounded down to the second. 64 | public sealed record CacheEntry(T? Value, DateTime Expiry) : ICacheEntry 65 | { 66 | /// 67 | /// The cached value. 68 | /// 69 | public T? Value { get; init; } = Value; 70 | 71 | /// 72 | /// The expiry date for the cache entry. 73 | /// 74 | public DateTime Expiry { get; init; } = new DateTime( 75 | Expiry.Year, Expiry.Month, Expiry.Day, Expiry.Hour, Expiry.Minute, Expiry.Second, DateTimeKind.Utc 76 | ); 77 | 78 | /// 79 | /// Creates a new with a default value. 80 | /// 81 | public CacheEntry() : this(default, DateTime.MinValue) { } 82 | 83 | /// 84 | /// Creates a new with the given and an expiry adjusted to the . 85 | /// 86 | /// The value to cache. 87 | /// The amount of time before the cache entry expires. 88 | internal CacheEntry(T? value, TimeSpan timeToLive) : this(value, DateTimeProvider.Now + timeToLive) { } 89 | } 90 | -------------------------------------------------------------------------------- /src/CacheTower/CacheEntryStatus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace CacheTower 6 | { 7 | /// 8 | /// Describes the status of the entry - whether it is expired, stale or access to an entry was a miss. 9 | /// 10 | internal enum CacheEntryStatus 11 | { 12 | /// 13 | /// When the cache entry is considered stale. 14 | /// 15 | Stale, 16 | /// 17 | /// When the cache entry is considered expired. 18 | /// 19 | Expired, 20 | /// 21 | /// When no cache entry was found. 22 | /// 23 | Miss 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CacheTower/CacheSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CacheTower 4 | { 5 | /// 6 | /// Cache settings used by a cache stack to evaluate whether a cache entry is stale or expired. 7 | /// 8 | public readonly record struct CacheSettings 9 | { 10 | /// 11 | /// How long till a cache entry is considered expired. 12 | /// 13 | public TimeSpan TimeToLive { get; } 14 | /// 15 | /// How long till a cache entry is considered stale. 16 | /// 17 | /// 18 | /// While optional, the cache will not perform a background refresh if is not set. 19 | /// 20 | public TimeSpan? StaleAfter { get; } 21 | 22 | /// 23 | /// Configures the cache entry to have a life of . 24 | /// 25 | /// 26 | /// How long till a cache entry is considered expired. 27 | /// Expired entries are removed from the cache and will force a foreground refresh if there is a cache miss. 28 | /// 29 | public CacheSettings(TimeSpan timeToLive) 30 | { 31 | TimeToLive = timeToLive; 32 | StaleAfter = null; 33 | } 34 | 35 | /// 36 | /// Configures the cache entry to have a life of and to be considered stale after . 37 | /// 38 | /// 39 | /// When there is a cache hit on a stale cache item, a background refresh will be performed. 40 | /// 41 | /// 42 | /// How long till a cache entry is considered expired. 43 | /// Expired entries are removed from the cache and will force a foreground refresh if there is a cache miss. 44 | /// 45 | /// 46 | /// How long till a cache entry is considered stale. 47 | /// When there is a cache hit on a stale entry, a background refresh is performed. 48 | /// 49 | /// Setting this too low will cause potentially unnecessary background refreshes. 50 | /// Setting this too high may limit the usefulness of a stale time. 51 | /// You will need to decide on the appropriate value based on your own usage. 52 | /// 53 | /// 54 | public CacheSettings(TimeSpan timeToLive, TimeSpan staleAfter) 55 | { 56 | TimeToLive = timeToLive; 57 | StaleAfter = staleAfter; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CacheTower/CacheStack{TContext}.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace CacheTower; 6 | 7 | /// 8 | /// A provides access to a in . 9 | /// This allows for the ability to inject dependencies during the cache refreshing process. 10 | /// 11 | /// The type of context that is passed to . 12 | /// 13 | public class CacheStack : CacheStack, ICacheStack 14 | { 15 | private readonly ICacheContextActivator CacheContextActivator; 16 | 17 | /// 18 | /// Creates a new with the provided and . 19 | /// 20 | /// The internal logger to use. 21 | /// The activator that provides the context. This is called for every cache item refresh. 22 | /// The to configure this cache stack. 23 | public CacheStack(ILogger? logger, ICacheContextActivator cacheContextActivator, CacheStackOptions options) : base(logger, options) 24 | { 25 | CacheContextActivator = cacheContextActivator ?? throw new ArgumentNullException(nameof(cacheContextActivator)); 26 | } 27 | 28 | /// 29 | /// Creates a new with the given , and . 30 | /// 31 | /// The internal logger to use. 32 | /// The activator that provides the context. This is called for every cache item refresh. 33 | /// The cache layers to use for the current cache stack. The layers should be ordered from the highest priority to the lowest. At least one cache layer is required. 34 | /// The cache extensions to use for the current cache stack. 35 | [Obsolete("Use constructor with 'CacheStackOptions'")] 36 | public CacheStack(ILogger? logger, ICacheContextActivator cacheContextActivator, ICacheLayer[] cacheLayers, ICacheExtension[] extensions) : base(logger, cacheLayers, extensions) 37 | { 38 | CacheContextActivator = cacheContextActivator ?? throw new ArgumentNullException(nameof(cacheContextActivator)); 39 | } 40 | 41 | /// 42 | public async ValueTask GetOrSetAsync(string cacheKey, Func> getter, CacheSettings settings) 43 | { 44 | ThrowIfDisposed(); 45 | 46 | if (cacheKey == null) 47 | { 48 | throw new ArgumentNullException(nameof(cacheKey)); 49 | } 50 | 51 | if (getter == null) 52 | { 53 | throw new ArgumentNullException(nameof(getter)); 54 | } 55 | 56 | return await GetOrSetAsync(cacheKey, async (old) => 57 | { 58 | using var scope = CacheContextActivator.BeginScope(); 59 | var context = (TContext)scope.Resolve(typeof(TContext)); 60 | return await getter(old, context).ConfigureAwait(false); 61 | }, settings).ConfigureAwait(false); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/CacheTower/CacheTower.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;netstandard2.1 5 | Cache Tower 6 | A multi-layered caching system for .NET 7 | inmemory,$(PackageBaseTags) 8 | James Turner 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/CacheTower/Extensions/AutoCleanupExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace CacheTower.Extensions 8 | { 9 | /// 10 | /// A basic delay-based cleanup extension for removing expired entries from cache layers. 11 | /// 12 | /// 13 | /// Not all cache layers manage their own cleanup of expired entries. 14 | /// This calls which triggers the cleanup on each layer. 15 | /// 16 | public class AutoCleanupExtension : ICacheExtension, IAsyncDisposable 17 | { 18 | /// 19 | /// The frequency at which an automatic cleanup is performed. 20 | /// 21 | public TimeSpan Frequency { get; } 22 | 23 | private Task? BackgroundTask; 24 | private readonly CancellationTokenSource TokenSource; 25 | 26 | /// 27 | /// Creates a new with the given . 28 | /// 29 | /// The frequency at which an automatic cleanup is performed. 30 | /// Optional cancellation token to end automatic cleanups. 31 | public AutoCleanupExtension(TimeSpan frequency, CancellationToken cancellationToken = default) 32 | { 33 | if (frequency <= TimeSpan.Zero) 34 | { 35 | throw new ArgumentOutOfRangeException(nameof(frequency), "Frequency must be greater than zero"); 36 | } 37 | 38 | Frequency = frequency; 39 | TokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, default); 40 | } 41 | 42 | /// 43 | public void Register(ICacheStack cacheStack) 44 | { 45 | if (BackgroundTask is not null) 46 | { 47 | throw new InvalidOperationException($"{nameof(AutoCleanupExtension)} can only be registered to one {nameof(ICacheStack)}"); 48 | } 49 | 50 | BackgroundTask = BackgroundCleanup(cacheStack); 51 | } 52 | 53 | private async Task BackgroundCleanup(ICacheStack cacheStack) 54 | { 55 | try 56 | { 57 | var cancellationToken = TokenSource.Token; 58 | while (!cancellationToken.IsCancellationRequested) 59 | { 60 | await Task.Delay(Frequency, cancellationToken).ConfigureAwait(false); 61 | cancellationToken.ThrowIfCancellationRequested(); 62 | await cacheStack.CleanupAsync().ConfigureAwait(false); 63 | } 64 | } 65 | catch (OperationCanceledException) { } 66 | } 67 | 68 | /// 69 | /// Cancels the automatic cleanup and releases all resources that were being used. 70 | /// 71 | /// 72 | public async ValueTask DisposeAsync() 73 | { 74 | TokenSource.Cancel(); 75 | 76 | if (BackgroundTask is not null && !BackgroundTask.IsFaulted) 77 | { 78 | await BackgroundTask.ConfigureAwait(false); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/CacheTower/Extensions/ExtensionContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace CacheTower.Extensions 7 | { 8 | internal class ExtensionContainer : ICacheExtension, ICacheChangeExtension, IDistributedLockExtension, IAsyncDisposable 9 | { 10 | private bool Disposed; 11 | 12 | private readonly bool HasDistributedLockExtension; 13 | private readonly IDistributedLockExtension? DistributedLockExtension; 14 | private readonly bool HasCacheChangeExtensions; 15 | private readonly ICacheChangeExtension[] CacheChangeExtensions; 16 | private readonly ICacheExtension[] AllExtensions; 17 | 18 | public ExtensionContainer(ICacheExtension[] extensions) 19 | { 20 | if (extensions != null && extensions.Length > 0) 21 | { 22 | var cacheChangeExtensions = new List(); 23 | 24 | foreach (var extension in extensions) 25 | { 26 | if (!HasDistributedLockExtension && extension is IDistributedLockExtension distributedLockExtension) 27 | { 28 | HasDistributedLockExtension = true; 29 | DistributedLockExtension = distributedLockExtension; 30 | } 31 | 32 | if (extension is ICacheChangeExtension cacheChangeExtension) 33 | { 34 | HasCacheChangeExtensions = true; 35 | cacheChangeExtensions.Add(cacheChangeExtension); 36 | } 37 | } 38 | 39 | CacheChangeExtensions = cacheChangeExtensions.ToArray(); 40 | AllExtensions = extensions; 41 | } 42 | else 43 | { 44 | CacheChangeExtensions = Array.Empty(); 45 | AllExtensions = CacheChangeExtensions; 46 | } 47 | } 48 | 49 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 50 | public void Register(ICacheStack cacheStack) 51 | { 52 | foreach (var extension in AllExtensions) 53 | { 54 | extension.Register(cacheStack); 55 | } 56 | } 57 | 58 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 59 | public ValueTask AwaitAccessAsync(string cacheKey) 60 | { 61 | if (HasDistributedLockExtension) 62 | { 63 | return DistributedLockExtension!.AwaitAccessAsync(cacheKey); 64 | } 65 | 66 | return new(DistributedLock.NotEnabled(cacheKey)); 67 | } 68 | 69 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 70 | public async ValueTask OnCacheUpdateAsync(string cacheKey, DateTime expiry, CacheUpdateType cacheUpdateType) 71 | { 72 | if (HasCacheChangeExtensions) 73 | { 74 | foreach (var extension in CacheChangeExtensions) 75 | { 76 | await extension.OnCacheUpdateAsync(cacheKey, expiry, cacheUpdateType).ConfigureAwait(false); 77 | } 78 | } 79 | } 80 | 81 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 82 | public async ValueTask OnCacheEvictionAsync(string cacheKey) 83 | { 84 | if (HasCacheChangeExtensions) 85 | { 86 | foreach (var extension in CacheChangeExtensions) 87 | { 88 | await extension.OnCacheEvictionAsync(cacheKey).ConfigureAwait(false); 89 | } 90 | } 91 | } 92 | 93 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 94 | public async ValueTask OnCacheFlushAsync() 95 | { 96 | if (HasCacheChangeExtensions) 97 | { 98 | foreach (var extension in CacheChangeExtensions) 99 | { 100 | await extension.OnCacheFlushAsync().ConfigureAwait(false); 101 | } 102 | } 103 | } 104 | 105 | public async ValueTask DisposeAsync() 106 | { 107 | if (Disposed) 108 | { 109 | return; 110 | } 111 | 112 | foreach (var extension in AllExtensions) 113 | { 114 | if (extension is IDisposable disposable) 115 | { 116 | disposable.Dispose(); 117 | } 118 | else if (extension is IAsyncDisposable asyncDisposable) 119 | { 120 | await asyncDisposable.DisposeAsync().ConfigureAwait(false); 121 | } 122 | } 123 | 124 | Disposed = true; 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/CacheTower/ICacheContextActivator.cs: -------------------------------------------------------------------------------- 1 | namespace CacheTower 2 | { 3 | /// 4 | /// Activator for creating a scope when resolving a context. 5 | /// 6 | public interface ICacheContextActivator 7 | { 8 | /// 9 | /// Begin a scope, and return the for resolving from 10 | /// 11 | /// A scope for the . 12 | ICacheContextScope BeginScope(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/CacheTower/ICacheContextScope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CacheTower 4 | { 5 | /// 6 | /// A scope for resolving a context. 7 | /// 8 | /// 9 | public interface ICacheContextScope : IDisposable 10 | { 11 | /// 12 | /// Function for resolving a type to a concrete implementation 13 | /// 14 | /// 15 | /// 16 | object Resolve(Type type); 17 | } 18 | } -------------------------------------------------------------------------------- /src/CacheTower/ICacheExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace CacheTower; 5 | 6 | /// 7 | /// An provides a method of extending the behaviour of Cache Tower. 8 | /// 9 | public interface ICacheExtension 10 | { 11 | /// 12 | /// Registers the provided to the current cache extension. 13 | /// 14 | /// The cache stack you want to register. 15 | void Register(ICacheStack cacheStack); 16 | } 17 | 18 | /// 19 | /// An exposes events into the inner workings of a cache stack. 20 | /// 21 | /// 22 | public interface ICacheChangeExtension : ICacheExtension 23 | { 24 | /// 25 | /// Triggers after a cache entry has been updated. 26 | /// 27 | /// The cache key for the entry that was updated. 28 | /// The new expiry date for the cache entry. 29 | /// The type of cache update that has occurred. 30 | /// 31 | ValueTask OnCacheUpdateAsync(string cacheKey, DateTime expiry, CacheUpdateType updateType); 32 | /// 33 | /// Triggers after a cache entry has been evicted. 34 | /// 35 | /// The cache key for the entry that was evicted. 36 | /// 37 | ValueTask OnCacheEvictionAsync(string cacheKey); 38 | /// 39 | /// Triggers after the cache stack is flushed. 40 | /// 41 | /// 42 | ValueTask OnCacheFlushAsync(); 43 | } 44 | /// 45 | /// Describes the type of cache update that the cache stack experienced. 46 | /// 47 | /// 48 | /// When you set a new cache entry, it isn't always known what the state of the cache currently is. 49 | /// Calling SetAsync doesn't check if it already exists. 50 | /// Calling GetOrSetAsync however is required to do such a check so this state can be passed along. 51 | /// 52 | public enum CacheUpdateType 53 | { 54 | /// 55 | /// When the state of an existing cache entry is unknown, a cache update could 56 | /// be triggered from adding a new entry or updating an existing one. 57 | /// 58 | AddOrUpdateEntry, 59 | /// 60 | /// When the state of an existing cache entry is known to not exist, a cache 61 | /// update is triggered specifically with the adding of an entry. 62 | /// 63 | AddEntry 64 | } 65 | 66 | /// 67 | /// An allows a cache lock to be acquired, avoiding cache stampedes across multiple systems. 68 | /// 69 | /// 70 | public interface IDistributedLockExtension : ICacheExtension 71 | { 72 | /// 73 | /// Performs a distributed lock where, for a specific , the task is completed in order based on when the lock is free. 74 | /// The owner of the distributed lock is released first and when completed (or timed out), any waiting consumers are completed too. 75 | /// 76 | /// The cache key to use perform a distributed lock on. 77 | /// A with details about whether the current is the lock owner. 78 | ValueTask AwaitAccessAsync(string cacheKey); 79 | } 80 | 81 | /// 82 | /// Represents a method that signals to release an existing lock for the specified . 83 | /// 84 | /// The cache key to release the lock for. 85 | /// 86 | public delegate ValueTask LockReleaseDelegate(string cacheKey); 87 | 88 | /// 89 | /// A distributed lock for a specific cache key. If the lock was acquired, it will be released on dispose. 90 | /// 91 | public readonly struct DistributedLock : IAsyncDisposable 92 | { 93 | /// 94 | /// The cache key for the distributed lock. 95 | /// 96 | public readonly string CacheKey; 97 | 98 | /// 99 | /// Whether the lock is owned or not by the current for the . 100 | /// When it is owned, the value refresh can be performed. 101 | /// 102 | public readonly bool IsLockOwner; 103 | 104 | private readonly LockReleaseDelegate? LockReleaseDelegate; 105 | 106 | private DistributedLock(string cacheKey, bool isLockOwner, LockReleaseDelegate? lockReleaseSignal) 107 | { 108 | CacheKey = cacheKey; 109 | IsLockOwner = isLockOwner; 110 | LockReleaseDelegate = lockReleaseSignal; 111 | } 112 | 113 | /// 114 | /// Creates a in the locked state for a specific . 115 | /// The is used on dispose to signal lock release. 116 | /// 117 | /// The cache key for the distributed lock. 118 | /// The delegate to trigger lock release. 119 | /// 120 | public static DistributedLock Locked(string cacheKey, LockReleaseDelegate lockReleaseSignal) => new(cacheKey, true, lockReleaseSignal); 121 | 122 | /// 123 | /// Creates a in the unlocked state for a specific . 124 | /// As there was no lock acquired, there is nothing to release on dispose. 125 | /// 126 | /// The cache key for the distributed lock. 127 | /// 128 | public static DistributedLock Unlocked(string cacheKey) => new(cacheKey, false, null); 129 | 130 | /// 131 | /// Creates a for when no distributed locking is enabled. 132 | /// It fakes itself as an acquired lock however has no lock release mechanism. 133 | /// 134 | /// The cache key for the distributed lock. 135 | /// 136 | internal static DistributedLock NotEnabled(string cacheKey) => new(cacheKey, true, null); 137 | 138 | /// 139 | /// If is , signals lock release. 140 | /// 141 | /// 142 | public ValueTask DisposeAsync() 143 | { 144 | if (LockReleaseDelegate is not null) 145 | { 146 | return LockReleaseDelegate(CacheKey); 147 | } 148 | 149 | return default; 150 | } 151 | } -------------------------------------------------------------------------------- /src/CacheTower/ICacheLayer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CacheTower 4 | { 5 | /// 6 | /// Cache layers represent individual types of caching solutions including in-memory, file-based and Redis. 7 | /// It is with cache layers that items are set, retrieved or evicted from the cache. 8 | /// 9 | public interface ICacheLayer 10 | { 11 | /// 12 | /// Flushes the cache layer, removing every item from the cache. 13 | /// 14 | /// 15 | ValueTask FlushAsync(); 16 | /// 17 | /// Triggers the cleanup of any cache entries that are expired. 18 | /// 19 | /// 20 | ValueTask CleanupAsync(); 21 | /// 22 | /// Removes an entry with the corresponding from the cache layer. 23 | /// 24 | /// The cache entry's key. 25 | /// 26 | ValueTask EvictAsync(string cacheKey); 27 | /// 28 | /// Retrieves the for a given . 29 | /// 30 | /// The type of value in the cache entry. 31 | /// The cache entry's key. 32 | /// The existing cache entry or null if no entry is found. 33 | ValueTask?> GetAsync(string cacheKey); 34 | /// 35 | /// Caches against the . 36 | /// 37 | /// The type of value in the cache entry. 38 | /// The cache entry's key. 39 | /// The cache entry to store. 40 | /// 41 | ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry); 42 | /// 43 | /// Retrieves the current availability status of the cache layer. 44 | /// This is used by to determine whether a value can even be cached at that moment in time. 45 | /// 46 | /// 47 | /// 48 | ValueTask IsAvailableAsync(string cacheKey); 49 | } 50 | 51 | /// 52 | /// 53 | /// A local cache layer represents a cache not shared with multiple instances of your application. 54 | /// For example, an in-memory cache layer would be an example of a local cache layer. 55 | /// 56 | public interface ILocalCacheLayer : ICacheLayer { } 57 | 58 | /// 59 | /// 60 | /// A distributed cache layer represents a cache that is shared with multiple instances of your application. 61 | /// For example, a Redis cache layer would be an example of a distributed cache layer. 62 | /// 63 | public interface IDistributedCacheLayer : ICacheLayer { } 64 | } 65 | -------------------------------------------------------------------------------- /src/CacheTower/ICacheSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.IO; 3 | 4 | namespace CacheTower 5 | { 6 | /// 7 | /// This abstraction allows us to use different encoding formats 8 | /// 9 | public interface ICacheSerializer 10 | { 11 | /// 12 | /// Serializes a to the specified . 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | void Serialize(Stream stream, T? value); 19 | 20 | /// 21 | /// Deserializes from the specified . 22 | /// 23 | /// 24 | /// 25 | /// 26 | T? Deserialize(Stream stream); 27 | } 28 | } -------------------------------------------------------------------------------- /src/CacheTower/ICacheStackAccessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace CacheTower; 7 | 8 | /// 9 | /// Provides access to a named implementation of . 10 | /// 11 | public interface ICacheStackAccessor 12 | { 13 | /// 14 | /// Creates or returns existing named base on the configured builder. 15 | /// 16 | /// The name of the that has been configured. 17 | /// 18 | ICacheStack GetCacheStack(string name); 19 | } 20 | 21 | /// 22 | /// Provides access to a named implementation of . 23 | /// 24 | /// The type of context that is passed during the cache entry generation process. 25 | public interface ICacheStackAccessor 26 | { 27 | /// 28 | /// Creates or returns existing named base on the configured builder. 29 | /// 30 | /// The name of the that has been configured. 31 | /// 32 | ICacheStack GetCacheStack(string name); 33 | } 34 | 35 | internal record NamedCacheStackProvider(string Name, Func Provider); 36 | internal class NamedCacheStackLookup 37 | { 38 | private readonly ConcurrentDictionary> cachedDependencies = new(StringComparer.Ordinal); 39 | private readonly Dictionary namedProviders; 40 | private readonly IServiceProvider serviceProvider; 41 | 42 | public NamedCacheStackLookup( 43 | IServiceProvider serviceProvider, 44 | IEnumerable namedProviders 45 | ) 46 | { 47 | this.serviceProvider = serviceProvider; 48 | this.namedProviders = namedProviders.ToDictionary(p => p.Name); 49 | } 50 | 51 | public ICacheStack GetCacheStack(string name) 52 | { 53 | if (!namedProviders.TryGetValue(name, out var dependencyProvider)) 54 | { 55 | throw new ArgumentException($"No ICacheStack is registered with the name \"{name}\""); 56 | } 57 | 58 | return cachedDependencies.GetOrAdd(name, name => new Lazy(() => dependencyProvider.Provider(serviceProvider))).Value; 59 | } 60 | } 61 | 62 | internal class CacheStackAccessor : ICacheStackAccessor 63 | { 64 | private readonly NamedCacheStackLookup cacheStackAccessor; 65 | 66 | public CacheStackAccessor(NamedCacheStackLookup cacheStackAccessor) 67 | { 68 | this.cacheStackAccessor = cacheStackAccessor; 69 | } 70 | 71 | public ICacheStack GetCacheStack(string name) => cacheStackAccessor.GetCacheStack(name); 72 | } 73 | 74 | internal class CacheStackAccessor : ICacheStackAccessor 75 | { 76 | private readonly NamedCacheStackLookup cacheStackAccessor; 77 | 78 | public CacheStackAccessor(NamedCacheStackLookup cacheStackAccessor) 79 | { 80 | this.cacheStackAccessor = cacheStackAccessor; 81 | } 82 | 83 | public ICacheStack GetCacheStack(string name) 84 | { 85 | if (cacheStackAccessor.GetCacheStack(name) is not ICacheStack cacheStack) 86 | { 87 | throw new InvalidOperationException($"Registered ICacheStack for \"{name}\" is not compatible with {typeof(ICacheStack)}"); 88 | } 89 | return cacheStack; 90 | } 91 | } -------------------------------------------------------------------------------- /src/CacheTower/Internal/CacheEntryKeyLock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | 6 | namespace CacheTower.Internal; 7 | 8 | internal readonly struct CacheEntryKeyLock 9 | { 10 | private readonly Dictionary?> keyLocks = new(StringComparer.Ordinal); 11 | 12 | public CacheEntryKeyLock() { } 13 | 14 | public bool AcquireLock(string cacheKey) 15 | { 16 | lock (keyLocks) 17 | { 18 | #if NETSTANDARD2_0 19 | var hasLock = !keyLocks.ContainsKey(cacheKey); 20 | if (hasLock) 21 | { 22 | keyLocks[cacheKey] = null; 23 | } 24 | return hasLock; 25 | #elif NETSTANDARD2_1 26 | return keyLocks.TryAdd(cacheKey, null); 27 | #endif 28 | } 29 | } 30 | 31 | public Task WaitAsync(string cacheKey) 32 | { 33 | TaskCompletionSource? completionSource; 34 | 35 | lock (keyLocks) 36 | { 37 | if (!keyLocks.TryGetValue(cacheKey, out completionSource) || completionSource == null) 38 | { 39 | completionSource = new TaskCompletionSource(); 40 | keyLocks[cacheKey] = completionSource; 41 | } 42 | } 43 | 44 | return completionSource.Task; 45 | } 46 | 47 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 48 | private bool TryRemove(string cacheKey, out TaskCompletionSource? completionSource) 49 | { 50 | lock (keyLocks) 51 | { 52 | #if NETSTANDARD2_0 53 | if (keyLocks.TryGetValue(cacheKey, out completionSource)) 54 | { 55 | keyLocks.Remove(cacheKey); 56 | return true; 57 | } 58 | return false; 59 | #elif NETSTANDARD2_1 60 | return keyLocks.Remove(cacheKey, out completionSource); 61 | #endif 62 | } 63 | } 64 | 65 | public void ReleaseLock(string cacheKey, ICacheEntry cacheEntry) 66 | { 67 | if (TryRemove(cacheKey, out var completionSource)) 68 | { 69 | completionSource?.TrySetResult(cacheEntry); 70 | } 71 | } 72 | 73 | public void ReleaseLock(string cacheKey, Exception exception) 74 | { 75 | if (TryRemove(cacheKey, out var completionSource)) 76 | { 77 | completionSource?.SetException(exception); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/CacheTower/Internal/DateTimeProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Threading; 4 | 5 | namespace CacheTower.Internal 6 | { 7 | internal static class DateTimeProvider 8 | { 9 | /// 10 | /// The current , updated every second. 11 | /// 12 | public static DateTime Now { get; private set; } = DateTime.UtcNow; 13 | 14 | /// 15 | /// Updates to the current value. This is automatically called by a timer every second. 16 | /// 17 | /// 18 | /// This is intended to only be triggered by the internal timer or by unit tests that require it. 19 | /// The reason why tests need it is due to the fast turn around of setting a value and testing the outcome. 20 | /// Real applications aren't immediately setting a cache value manually, calling and then comparing whether the results are the same. 21 | /// The alternative for the tests is just "waiting" an extra second between setting a value and retrieving it however that makes the testing slower and the tests more confusing. 22 | /// 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public static void UpdateTime() => Now = DateTime.UtcNow; 25 | 26 | [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0052:Remove unread private members", Justification = "Establishes timer and prevents it being garbage collected")] 27 | private static readonly Timer DateTimeTimer = new(state => UpdateTime(), null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/CacheTower/Internal/MD5HashUtility.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace CacheTower.Internal; 6 | 7 | internal static class MD5HashUtility 8 | { 9 | [ThreadStatic] 10 | private static MD5? ThreadInstance; 11 | 12 | private static MD5 HashAlgorithm => ThreadInstance ??= MD5.Create(); 13 | 14 | #if NETSTANDARD2_0 15 | public static unsafe string ComputeHash(string value) 16 | { 17 | var bytes = Encoding.UTF8.GetBytes(value); 18 | var hashBytes = HashAlgorithm.ComputeHash(bytes); 19 | 20 | #elif NETSTANDARD2_1 21 | public static unsafe string ComputeHash(ReadOnlySpan value) 22 | { 23 | var encoding = Encoding.UTF8; 24 | var bytesRequired = encoding.GetByteCount(value); 25 | Span bytes = stackalloc byte[bytesRequired]; 26 | encoding.GetBytes(value, bytes); 27 | 28 | Span hashBytes = stackalloc byte[16]; 29 | HashAlgorithm.TryComputeHash(bytes, hashBytes, out var _); 30 | #endif 31 | 32 | //Based on byte conversion implementation in BitConverter (but with the dash stripped) 33 | //https://github.com/dotnet/coreclr/blob/fbc11ea6afdaa2fe7b9377446d6bb0bd447d5cb5/src/mscorlib/shared/System/BitConverter.cs#L409-L440 34 | static char GetHexValue(int i) 35 | { 36 | if (i < 10) 37 | { 38 | return (char)(i + '0'); 39 | } 40 | 41 | return (char)(i - 10 + 'A'); 42 | } 43 | 44 | const int charArrayLength = 32; 45 | var charArrayPtr = stackalloc char[charArrayLength]; 46 | 47 | var charPtr = charArrayPtr; 48 | for (var i = 0; i < 16; i++) 49 | { 50 | var hashByte = hashBytes[i]; 51 | *charPtr++ = GetHexValue(hashByte >> 4); 52 | *charPtr++ = GetHexValue(hashByte & 0xF); 53 | } 54 | 55 | return new string(charArrayPtr, 0, charArrayLength); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/CacheTower/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace System.Runtime.CompilerServices 4 | { 5 | /// 6 | /// Reserved to be used by the compiler for tracking metadata. 7 | /// This class should not be used by developers in source code. 8 | /// 9 | [EditorBrowsable(EditorBrowsableState.Never)] 10 | internal static class IsExternalInit 11 | { 12 | } 13 | } -------------------------------------------------------------------------------- /src/CacheTower/Providers/FileSystem/FileCacheLayerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CacheTower.Providers.FileSystem; 4 | 5 | /// 6 | /// Options for controlling a . 7 | /// 8 | /// The directory to store the cache in. 9 | /// The serializer to use for the data. 10 | public record struct FileCacheLayerOptions( 11 | string DirectoryPath, 12 | ICacheSerializer Serializer 13 | ) 14 | { 15 | /// 16 | /// The default manifest save interval of 30 seconds. 17 | /// 18 | public static readonly TimeSpan DefaultManifestSaveInterval = TimeSpan.FromSeconds(30); 19 | 20 | /// 21 | /// The time interval controlling how often the cache manifest is saved to disk. 22 | /// 23 | public TimeSpan ManifestSaveInterval { get; init; } = DefaultManifestSaveInterval; 24 | 25 | /// 26 | /// Options for controlling a . 27 | /// 28 | /// The directory to store the cache in. 29 | /// The serializer to use for the data. 30 | /// The time interval controlling how often the cache manifest is saved to disk. 31 | public FileCacheLayerOptions( 32 | string directoryPath, 33 | ICacheSerializer serializer, 34 | TimeSpan manifestSaveInterval 35 | ) : this(directoryPath, serializer) 36 | { 37 | ManifestSaveInterval = manifestSaveInterval; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/CacheTower/Providers/FileSystem/ManifestEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CacheTower.Providers.FileSystem 4 | { 5 | /// 6 | /// The manifest entry for a file system based cache. 7 | /// 8 | /// The file name that contains the cached data. 9 | /// The expiry date of the cached value. 10 | public readonly record struct ManifestEntry(string? FileName, DateTime Expiry) 11 | { 12 | /// 13 | /// The file name that contains the cached data. 14 | /// 15 | public string? FileName { get; init; } = FileName; 16 | /// 17 | /// The expiry date of the cached value. 18 | /// 19 | public DateTime Expiry { get; init; } = Expiry; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/CacheTower/Providers/Memory/MemoryCacheLayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading.Tasks; 4 | using CacheTower.Internal; 5 | 6 | namespace CacheTower.Providers.Memory 7 | { 8 | /// 9 | /// The allows fast, local memory caching. 10 | /// Cached data is stored as a reference internally, rather than serialized like other caching systems do. 11 | /// This provides far greater performance than other in-memory solutions. 12 | /// 13 | /// Because of the stored reference, it is strongly recommend to not modify instances of a cached value. 14 | /// Instead, if there is an updated state to store, use the appropriate setter methods on the cache stack or layer. 15 | /// 16 | /// 17 | /// 18 | public class MemoryCacheLayer : ILocalCacheLayer 19 | { 20 | private readonly ConcurrentDictionary Cache = new(StringComparer.Ordinal); 21 | 22 | /// 23 | public ValueTask CleanupAsync() 24 | { 25 | var currentTime = DateTimeProvider.Now; 26 | 27 | foreach (var cachePair in Cache) 28 | { 29 | var cacheEntry = cachePair.Value; 30 | if (cacheEntry.Expiry < currentTime) 31 | { 32 | Cache.TryRemove(cachePair.Key, out _); 33 | } 34 | } 35 | 36 | return new ValueTask(); 37 | } 38 | 39 | /// 40 | public ValueTask EvictAsync(string cacheKey) 41 | { 42 | Cache.TryRemove(cacheKey, out _); 43 | return new ValueTask(); 44 | } 45 | 46 | /// 47 | public ValueTask FlushAsync() 48 | { 49 | Cache.Clear(); 50 | return new ValueTask(); 51 | } 52 | 53 | /// 54 | public ValueTask?> GetAsync(string cacheKey) 55 | { 56 | if (Cache.TryGetValue(cacheKey, out var cacheEntry)) 57 | { 58 | return new ValueTask?>(cacheEntry as CacheEntry); 59 | } 60 | 61 | return new ValueTask?>(default(CacheEntry)); 62 | } 63 | 64 | /// 65 | public ValueTask IsAvailableAsync(string cacheKey) 66 | { 67 | return new ValueTask(true); 68 | } 69 | 70 | /// 71 | public ValueTask SetAsync(string cacheKey, CacheEntry cacheEntry) 72 | { 73 | Cache[cacheKey] = cacheEntry; 74 | return new ValueTask(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/CacheTower/Serializers/CacheSerializationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CacheTower.Serializers; 4 | 5 | /// 6 | /// An exception for any cache serialization exceptions that occur. 7 | /// 8 | public class CacheSerializationException : Exception 9 | { 10 | /// 11 | /// Creates a new . 12 | /// 13 | public CacheSerializationException() : base() { } 14 | /// 15 | /// Creates a new with the specified . 16 | /// 17 | /// The error message 18 | public CacheSerializationException(string message) : base(message) { } 19 | /// 20 | /// Creates a new with the specified and . 21 | /// 22 | /// The error message 23 | /// The inner exception 24 | public CacheSerializationException(string message, Exception innerException) : base(message, innerException) { } 25 | } 26 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Turner Software 5 | 6 | $(AssemblyName) 7 | true 8 | MIT 9 | icon.png 10 | https://github.com/TurnerSoftware/CacheTower 11 | caching;cache;multi-layer 12 | 13 | 14 | true 15 | true 16 | embedded 17 | 18 | Latest 19 | enable 20 | true 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | <_Parameter1>true 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/CacheContextActivatorTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace CacheTower.Tests; 5 | 6 | [TestClass] 7 | public class CacheContextActivatorTests 8 | { 9 | private class MyResolvableType 10 | { 11 | public string Description { get; set; } 12 | } 13 | 14 | private void AssertActivator(ICacheContextActivator activator, string expectedDescription) 15 | { 16 | using var scope = activator.BeginScope(); 17 | var result = (MyResolvableType)scope.Resolve(typeof(MyResolvableType)); 18 | Assert.AreEqual(expectedDescription, result.Description); 19 | } 20 | 21 | [TestMethod] 22 | public void ServiceProviderContextActivator_Resolves() 23 | { 24 | var serviceCollection = new ServiceCollection(); 25 | serviceCollection.AddScoped(p => new MyResolvableType 26 | { 27 | Description = "ServiceProvider" 28 | }); 29 | var serviceProvider = serviceCollection.BuildServiceProvider(); 30 | var activator = new ServiceProviderContextActivator(serviceProvider); 31 | 32 | AssertActivator(activator, "ServiceProvider"); 33 | } 34 | 35 | [TestMethod] 36 | public void FuncCacheContextActivator_Resolves() 37 | { 38 | var activator = new FuncCacheContextActivator(() => new MyResolvableType 39 | { 40 | Description = "FuncCache" 41 | }); 42 | 43 | AssertActivator(activator, "FuncCache"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/CacheEntryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | 8 | namespace CacheTower.Tests 9 | { 10 | [TestClass] 11 | public class CacheEntryTests 12 | { 13 | [TestMethod] 14 | public void GetStaleDate_WithStaleAfter() 15 | { 16 | var expiry = DateTime.UtcNow; 17 | var entry = new CacheEntry(0, expiry); 18 | expiry = entry.Expiry; 19 | 20 | var staleDate = entry.GetStaleDate(new CacheSettings(TimeSpan.FromDays(3), TimeSpan.FromDays(2))); 21 | Assert.AreEqual(expiry.AddDays(-1), staleDate); 22 | } 23 | 24 | [TestMethod] 25 | public void GetStaleDate_WithoutStaleAfter() 26 | { 27 | var expiry = DateTime.UtcNow; 28 | var entry = new CacheEntry(0, expiry); 29 | expiry = entry.Expiry; 30 | 31 | var staleDate = entry.GetStaleDate(new CacheSettings(TimeSpan.FromDays(3))); 32 | Assert.AreEqual(expiry, staleDate); 33 | } 34 | 35 | [TestMethod] 36 | public void EqualityTests_AreEqual() 37 | { 38 | var utcNow = DateTime.UtcNow; 39 | Assert.AreEqual(new CacheEntry(0, utcNow), new CacheEntry(0, utcNow)); 40 | Assert.AreEqual(new CacheEntry(string.Empty, utcNow), new CacheEntry(string.Empty, utcNow)); 41 | } 42 | 43 | [TestMethod] 44 | public void EqualityTests_AreNotEqual() 45 | { 46 | var utcNow = DateTime.UtcNow; 47 | Assert.AreNotEqual(new CacheEntry(0, utcNow), new CacheEntry(0, utcNow.AddSeconds(1))); 48 | Assert.AreNotEqual(new CacheEntry(0, utcNow), new CacheEntry(1, utcNow)); 49 | 50 | Assert.IsFalse(new CacheEntry(0, utcNow).Equals(null)); 51 | Assert.IsFalse(new CacheEntry(0, utcNow).Equals(string.Empty)); 52 | } 53 | 54 | [TestMethod] 55 | public void EqualityTests_HashCode() 56 | { 57 | var utcNow = DateTime.UtcNow; 58 | Assert.AreEqual(new CacheEntry(0, utcNow).GetHashCode(), new CacheEntry(0, utcNow).GetHashCode()); 59 | Assert.AreEqual(new CacheEntry(string.Empty, utcNow).GetHashCode(), new CacheEntry(string.Empty, utcNow).GetHashCode()); 60 | Assert.AreNotEqual(new CacheEntry(0, utcNow).GetHashCode(), new CacheEntry(0, utcNow.AddSeconds(1)).GetHashCode()); 61 | Assert.AreNotEqual(new CacheEntry(0, utcNow).GetHashCode(), new CacheEntry(1, utcNow).GetHashCode()); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/CacheStackContextTests.cs: -------------------------------------------------------------------------------- 1 | using CacheTower.Providers.Memory; 2 | using System; 3 | using System.Threading.Tasks; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace CacheTower.Tests 7 | { 8 | [TestClass] 9 | public class CacheStackContextTests : TestBase 10 | { 11 | [TestMethod, ExpectedException(typeof(ArgumentNullException))] 12 | public async Task GetOrSet_ThrowsOnNullKey() 13 | { 14 | await using var cacheStack = new CacheStack(null, new FuncCacheContextActivator(() => null), new(new[] { new MemoryCacheLayer() })); 15 | await cacheStack.GetOrSetAsync(null, (old, context) => Task.FromResult(5), new CacheSettings(TimeSpan.FromDays(1))); 16 | } 17 | [TestMethod, ExpectedException(typeof(ArgumentNullException))] 18 | public async Task GetOrSet_ThrowsOnNullGetter() 19 | { 20 | await using var cacheStack = new CacheStack(null, new FuncCacheContextActivator(() => null), new(new[] { new MemoryCacheLayer() })); 21 | await cacheStack.GetOrSetAsync("MyCacheKey", null, new CacheSettings(TimeSpan.FromDays(1))); 22 | } 23 | [TestMethod] 24 | public async Task GetOrSet_CacheMiss_ContextHasValue() 25 | { 26 | await using var cacheStack = new CacheStack(null, new FuncCacheContextActivator(() => 123), new(new[] { new MemoryCacheLayer() })); 27 | var result = await cacheStack.GetOrSetAsync("GetOrSet_CacheMiss_ContextHasValue", (oldValue, context) => 28 | { 29 | Assert.AreEqual(123, context); 30 | return Task.FromResult(5); 31 | }, new CacheSettings(TimeSpan.FromDays(1))); 32 | 33 | Assert.AreEqual(5, result); 34 | } 35 | [TestMethod] 36 | public async Task GetOrSet_CacheMiss_ContextFactoryCalledEachTime() 37 | { 38 | var contextValue = 0; 39 | await using var cacheStack = new CacheStack(null, new FuncCacheContextActivator(() => contextValue++), new(new[] { new MemoryCacheLayer() })); 40 | 41 | var result1 = await cacheStack.GetOrSetAsync("GetOrSet_CacheMiss_ContextFactoryCalledEachTime_1", (oldValue, context) => 42 | { 43 | Assert.AreEqual(0, context); 44 | return Task.FromResult(5); 45 | }, new CacheSettings(TimeSpan.FromDays(1))); 46 | Assert.AreEqual(5, result1); 47 | 48 | var result2 = await cacheStack.GetOrSetAsync("GetOrSet_CacheMiss_ContextFactoryCalledEachTime_2", (oldValue, context) => 49 | { 50 | Assert.AreEqual(1, context); 51 | return Task.FromResult(5); 52 | }, new CacheSettings(TimeSpan.FromDays(1))); 53 | Assert.AreEqual(5, result2); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/CacheTower.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net462;net6.0;net7.0 5 | false 6 | Latest 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/ComplexTypeCachingObjects.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using ProtoBuf; 5 | 6 | namespace CacheTower.Tests; 7 | 8 | internal static class SequenceComparison 9 | { 10 | public static bool Compare(IEnumerable x, IEnumerable y) 11 | { 12 | if (x is not null && y is not null) 13 | { 14 | return x.SequenceEqual(y); 15 | } 16 | return x is null && y is null; 17 | } 18 | } 19 | 20 | [ProtoContract] 21 | public record BasicTypeCaching_TypeOne 22 | { 23 | [ProtoMember(1)] 24 | public string ExampleString { get; set; } 25 | } 26 | 27 | [ProtoContract] 28 | public class ComplexTypeCaching_TypeOne : IEquatable 29 | { 30 | [ProtoMember(1)] 31 | public int ExampleNumber { get; set; } 32 | [ProtoMember(2)] 33 | public string ExampleString { get; set; } 34 | [ProtoMember(3)] 35 | public List ListOfNumbers { get; set; } 36 | 37 | public bool Equals(ComplexTypeCaching_TypeOne other) 38 | { 39 | if (other == null) 40 | { 41 | return false; 42 | } 43 | 44 | return ExampleNumber == other.ExampleNumber && 45 | ExampleString == other.ExampleString && 46 | SequenceComparison.Compare(ListOfNumbers, other.ListOfNumbers); 47 | } 48 | 49 | public override bool Equals(object obj) 50 | { 51 | if (obj is ComplexTypeCaching_TypeOne complexType) 52 | { 53 | return Equals(complexType); 54 | } 55 | 56 | return false; 57 | } 58 | 59 | public override int GetHashCode() 60 | { 61 | return ExampleNumber.GetHashCode() ^ 62 | ExampleString.GetHashCode() ^ 63 | ListOfNumbers.GetHashCode(); 64 | } 65 | } 66 | 67 | [ProtoContract] 68 | public class ComplexTypeCaching_TypeTwo : IEquatable 69 | { 70 | [ProtoMember(1)] 71 | public string ExampleString { get; set; } 72 | [ProtoMember(2)] 73 | public ComplexTypeCaching_TypeOne[] ArrayOfObjects { get; set; } 74 | [ProtoMember(3)] 75 | public Dictionary DictionaryOfNumbers { get; set; } 76 | 77 | public bool Equals(ComplexTypeCaching_TypeTwo other) 78 | { 79 | if (other == null) 80 | { 81 | return false; 82 | } 83 | 84 | return ExampleString == other.ExampleString && 85 | SequenceComparison.Compare(ArrayOfObjects, other.ArrayOfObjects) && 86 | SequenceComparison.Compare(DictionaryOfNumbers, other.DictionaryOfNumbers); 87 | } 88 | 89 | public override bool Equals(object obj) 90 | { 91 | if (obj is ComplexTypeCaching_TypeTwo complexType) 92 | { 93 | return Equals(complexType); 94 | } 95 | 96 | return false; 97 | } 98 | 99 | public override int GetHashCode() 100 | { 101 | return ExampleString.GetHashCode() ^ 102 | ArrayOfObjects.GetHashCode() ^ 103 | DictionaryOfNumbers.GetHashCode(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Extensions/AutoCleanupExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using CacheTower.Extensions; 8 | using CacheTower.Providers.Memory; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | using NSubstitute; 11 | using NSubstitute.ReceivedExtensions; 12 | 13 | namespace CacheTower.Tests.Extensions 14 | { 15 | [TestClass] 16 | public class AutoCleanupExtensionTests : TestBase 17 | { 18 | [TestMethod, ExpectedException(typeof(ArgumentOutOfRangeException))] 19 | public void ThrowForInvalidFrequency() 20 | { 21 | new AutoCleanupExtension(TimeSpan.Zero); 22 | } 23 | 24 | [TestMethod, ExpectedException(typeof(InvalidOperationException))] 25 | public async Task ThrowForRegisteringTwoCacheStacks() 26 | { 27 | await using var extension = new AutoCleanupExtension(TimeSpan.FromSeconds(30)); 28 | //Will register as part of the CacheStack constructor 29 | await using var cacheStack = new CacheStack(null, new(new[] { new MemoryCacheLayer() }) { Extensions = new[] { extension } }); 30 | //Force the second register manually 31 | extension.Register(cacheStack); 32 | } 33 | 34 | [TestMethod] 35 | public async Task RunsBackgroundCleanup() 36 | { 37 | await using var extension = new AutoCleanupExtension(TimeSpan.FromMilliseconds(500)); 38 | var cacheStackMock = Substitute.For(); 39 | extension.Register(cacheStackMock); 40 | await Task.Delay(TimeSpan.FromSeconds(2)); 41 | await cacheStackMock.Received(Quantity.Within(2, int.MaxValue)).CleanupAsync(); 42 | } 43 | 44 | [TestMethod] 45 | public async Task BackgroundCleanupObeysCancel() 46 | { 47 | await using var extension = new AutoCleanupExtension(TimeSpan.FromMilliseconds(500), new CancellationToken(true)); 48 | var cacheStackMock = Substitute.For(); 49 | extension.Register(cacheStackMock); 50 | await Task.Delay(TimeSpan.FromSeconds(2)); 51 | await cacheStackMock.DidNotReceive().CleanupAsync(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Extensions/ExtensionContainerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CacheTower.Extensions; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using NSubstitute; 6 | 7 | namespace CacheTower.Tests.Extensions 8 | { 9 | [TestClass] 10 | public class ExtensionContainerTests : TestBase 11 | { 12 | [TestMethod] 13 | public async Task AcceptsNullExtensions() 14 | { 15 | await using var container = new ExtensionContainer(null); 16 | } 17 | 18 | [TestMethod] 19 | public async Task AcceptsEmptyExtensions() 20 | { 21 | await using var container = new ExtensionContainer(Array.Empty()); 22 | } 23 | 24 | [TestMethod] 25 | public async Task DistributedLockExtension() 26 | { 27 | var cacheStackMock = Substitute.For(); 28 | var distributedLockMock = Substitute.For(); 29 | await using var container = new ExtensionContainer(new[] { distributedLockMock }); 30 | 31 | container.Register(cacheStackMock); 32 | 33 | var distributedLock = await container.AwaitAccessAsync("DistributedLockCacheKey"); 34 | 35 | distributedLockMock.Received(1).Register(cacheStackMock); 36 | await distributedLockMock.Received(1).AwaitAccessAsync("DistributedLockCacheKey"); 37 | } 38 | 39 | [TestMethod] 40 | public async Task CacheChangeExtension_Update() 41 | { 42 | var cacheStackMock = Substitute.For(); 43 | var valueRefreshMock = Substitute.For(); 44 | await using var container = new ExtensionContainer(new[] { valueRefreshMock }); 45 | 46 | container.Register(cacheStackMock); 47 | 48 | var expiry = DateTime.UtcNow.AddDays(1); 49 | 50 | await container.OnCacheUpdateAsync("CacheChangeKey", expiry, CacheUpdateType.AddEntry); 51 | 52 | valueRefreshMock.Received(1).Register(cacheStackMock); 53 | await valueRefreshMock.Received(1).OnCacheUpdateAsync("CacheChangeKey", expiry, CacheUpdateType.AddEntry); 54 | } 55 | 56 | [TestMethod] 57 | public async Task CacheChangeExtension_Eviction() 58 | { 59 | var cacheStackMock = Substitute.For(); 60 | var valueRefreshMock = Substitute.For(); 61 | await using var container = new ExtensionContainer(new[] { valueRefreshMock }); 62 | 63 | container.Register(cacheStackMock); 64 | 65 | var expiry = DateTime.UtcNow.AddDays(1); 66 | 67 | await container.OnCacheEvictionAsync("CacheChangeKey"); 68 | 69 | valueRefreshMock.Received(1).Register(cacheStackMock); 70 | await valueRefreshMock.Received(1).OnCacheEvictionAsync("CacheChangeKey"); 71 | } 72 | 73 | [TestMethod] 74 | public async Task CacheChangeExtension_Flush() 75 | { 76 | var cacheStackMock = Substitute.For(); 77 | var valueRefreshMock = Substitute.For(); 78 | await using var container = new ExtensionContainer(new[] { valueRefreshMock }); 79 | 80 | container.Register(cacheStackMock); 81 | 82 | var expiry = DateTime.UtcNow.AddDays(1); 83 | 84 | await container.OnCacheFlushAsync(); 85 | 86 | valueRefreshMock.Received(1).Register(cacheStackMock); 87 | await valueRefreshMock.Received(1).OnCacheFlushAsync(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/FuncCacheContextActivator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CacheTower.Tests; 4 | 5 | internal class FuncCacheContextActivator : ICacheContextActivator 6 | { 7 | private readonly Func Resolver; 8 | 9 | public FuncCacheContextActivator(Func resolver) 10 | { 11 | Resolver = resolver; 12 | } 13 | 14 | public ICacheContextScope BeginScope() 15 | { 16 | return new FuncCacheContextScope(Resolver); 17 | } 18 | } 19 | 20 | internal class FuncCacheContextScope : ICacheContextScope 21 | { 22 | private readonly Func Resolver; 23 | 24 | public FuncCacheContextScope(Func resolver) 25 | { 26 | Resolver = resolver; 27 | } 28 | 29 | public object Resolve(Type type) 30 | { 31 | return Resolver()!; 32 | } 33 | 34 | public void Dispose() 35 | { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Providers/BaseCacheLayerTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace CacheTower.Tests.Providers 7 | { 8 | public abstract class BaseCacheLayerTests : TestBase 9 | { 10 | protected static async Task AssertPersistentGetSetCacheAsync(Func cacheLayerFactory) where TCacheLayer : ICacheLayer, IAsyncDisposable 11 | { 12 | await using (var cacheLayerOne = cacheLayerFactory()) 13 | { 14 | var cacheEntry = new CacheEntry(12, TimeSpan.FromDays(1)); 15 | await cacheLayerOne.SetAsync("AssertPersistentGetSetCache", cacheEntry); 16 | } 17 | 18 | await using var cacheLayerTwo = cacheLayerFactory(); 19 | var persistedCacheEntry = await cacheLayerTwo.GetAsync("AssertPersistentGetSetCache"); 20 | Assert.AreEqual(12, persistedCacheEntry.Value); 21 | } 22 | 23 | protected static async Task AssertGetSetCacheAsync(ICacheLayer cacheLayer) 24 | { 25 | var cacheEntry = new CacheEntry(12, TimeSpan.FromDays(1)); 26 | await cacheLayer.SetAsync("AssertGetSetCache", cacheEntry); 27 | var cacheEntryGet = await cacheLayer.GetAsync("AssertGetSetCache"); 28 | 29 | Assert.AreEqual(cacheEntry, cacheEntryGet, "Set value in cache doesn't match retrieved value"); 30 | } 31 | 32 | protected static async Task AssertCacheAvailabilityAsync(ICacheLayer cacheLayer, bool expected) 33 | { 34 | Assert.AreEqual(expected, await cacheLayer.IsAvailableAsync("AnyCacheKey-DoesntNeedToExist")); 35 | } 36 | 37 | protected static async Task AssertCacheEvictionAsync(ICacheLayer cacheLayer) 38 | { 39 | var cacheEntry = new CacheEntry(77, TimeSpan.FromDays(1)); 40 | await cacheLayer.SetAsync("AssertCacheEviction-ToEvict", cacheEntry); 41 | await cacheLayer.SetAsync("AssertCacheEviction-ToKeep", cacheEntry); 42 | 43 | var cacheEntryGetPreEviction1 = await cacheLayer.GetAsync("AssertCacheEviction-ToEvict"); 44 | var cacheEntryGetPreEviction2 = await cacheLayer.GetAsync("AssertCacheEviction-ToKeep"); 45 | Assert.IsNotNull(cacheEntryGetPreEviction1, "Value not set in cache"); 46 | Assert.IsNotNull(cacheEntryGetPreEviction2, "Value not set in cache"); 47 | 48 | await cacheLayer.EvictAsync("AssertCacheEviction-ToEvict"); 49 | 50 | var cacheEntryGetPostEviction1 = await cacheLayer.GetAsync("AssertCacheEviction-ToEvict"); 51 | var cacheEntryGetPostEviction2 = await cacheLayer.GetAsync("AssertCacheEviction-ToKeep"); 52 | Assert.IsNull(cacheEntryGetPostEviction1, "Didn't evict value that should have been"); 53 | Assert.IsNotNull(cacheEntryGetPostEviction2, "Evicted entry that should have been kept"); 54 | } 55 | 56 | protected static async Task AssertCacheCleanupAsync(ICacheLayer cacheLayer) 57 | { 58 | async Task> DoCleanupTest(DateTime dateTime) 59 | { 60 | var cacheKey = $"AssertCacheCleanup-(DateTime:{dateTime})"; 61 | 62 | var cacheEntry = new CacheEntry(98, dateTime); 63 | await cacheLayer.SetAsync(cacheKey, cacheEntry); 64 | 65 | await cacheLayer.CleanupAsync(); 66 | 67 | return await cacheLayer.GetAsync(cacheKey); 68 | } 69 | 70 | Assert.IsNotNull(await DoCleanupTest(DateTime.UtcNow.AddDays(1)), "Cleanup removed entry that was still live"); 71 | Assert.IsNull(await DoCleanupTest(DateTime.UtcNow.AddDays(-1)), "Cleanup kept entry past the end of life"); 72 | } 73 | 74 | protected static async Task AssertCacheFlushAsync(ICacheLayer cacheLayer) 75 | { 76 | var cacheEntry = new CacheEntry(77, TimeSpan.FromDays(1)); 77 | await cacheLayer.SetAsync("AssertCacheFlush-ToFlush", cacheEntry); 78 | 79 | var cacheEntryGetPreFlush = await cacheLayer.GetAsync("AssertCacheFlush-ToFlush"); 80 | Assert.IsNotNull(cacheEntryGetPreFlush, "Value not set in cache"); 81 | 82 | await cacheLayer.FlushAsync(); 83 | 84 | var cacheEntryGetPostFlush = await cacheLayer.GetAsync("AssertCacheFlush-ToFlush"); 85 | Assert.IsNull(cacheEntryGetPostFlush, "Didn't flush value that should have been"); 86 | } 87 | 88 | protected static async Task AssertComplexTypeCachingAsync(ICacheLayer cacheLayer) 89 | { 90 | var complexTypeOneEntry = new CacheEntry(new ComplexTypeCaching_TypeOne 91 | { 92 | ExampleString = "Hello World", 93 | ExampleNumber = 99, 94 | ListOfNumbers = new List() { 1, 2, 4, 8 } 95 | }, TimeSpan.FromDays(1)); 96 | await cacheLayer.SetAsync("ComplexTypeOne", complexTypeOneEntry); 97 | var complexTypeOneEntryGet = await cacheLayer.GetAsync("ComplexTypeOne"); 98 | 99 | Assert.AreEqual(complexTypeOneEntry, complexTypeOneEntryGet, "Set value in cache doesn't match retrieved value"); 100 | 101 | var complexTypeTwoEntry = new CacheEntry(new ComplexTypeCaching_TypeTwo 102 | { 103 | ExampleString = "Hello World", 104 | ArrayOfObjects = new[] { complexTypeOneEntry.Value }, 105 | DictionaryOfNumbers = new Dictionary() { { "A", 1 }, { "Z", 26 } } 106 | }, TimeSpan.FromDays(1)); 107 | await cacheLayer.SetAsync("ComplexTypeTwo", complexTypeTwoEntry); 108 | var complexTypeTwoEntryGet = await cacheLayer.GetAsync("ComplexTypeTwo"); 109 | 110 | Assert.AreEqual(complexTypeTwoEntry, complexTypeTwoEntryGet, "Set value in cache doesn't match retrieved value"); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Providers/Database/MongoDbCacheLayerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CacheTower.Providers.Database.MongoDB; 4 | using CacheTower.Tests.Utils; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | using MongoFramework; 7 | using MongoFramework.Infrastructure.Indexing; 8 | using NSubstitute; 9 | 10 | namespace CacheTower.Tests.Providers.Database; 11 | 12 | [TestClass] 13 | public class MongoDbCacheLayerTests : BaseCacheLayerTests 14 | { 15 | [TestInitialize] 16 | public async Task Setup() 17 | { 18 | await MongoDbHelper.DropDatabaseAsync(); 19 | } 20 | 21 | [TestCleanup] 22 | public async Task Cleanup() 23 | { 24 | await MongoDbHelper.DropDatabaseAsync(); 25 | } 26 | 27 | [TestMethod] 28 | public async Task GetSetCache() 29 | { 30 | await AssertGetSetCacheAsync(new MongoDbCacheLayer(MongoDbHelper.GetConnection())); 31 | } 32 | 33 | [TestMethod] 34 | public async Task IsCacheAvailable() 35 | { 36 | EntityIndexWriter.ClearCache(); 37 | 38 | await AssertCacheAvailabilityAsync(new MongoDbCacheLayer(MongoDbHelper.GetConnection()), true); 39 | 40 | var connectionMock = Substitute.For(); 41 | connectionMock.GetDatabase().Returns(x => throw new Exception()); 42 | EntityIndexWriter.ClearCache(); 43 | 44 | await AssertCacheAvailabilityAsync(new MongoDbCacheLayer(connectionMock), false); 45 | } 46 | 47 | [TestMethod] 48 | public async Task EvictFromCache() 49 | { 50 | await AssertCacheEvictionAsync(new MongoDbCacheLayer(MongoDbHelper.GetConnection())); 51 | } 52 | 53 | [TestMethod] 54 | public async Task FlushFromCache() 55 | { 56 | await AssertCacheFlushAsync(new MongoDbCacheLayer(MongoDbHelper.GetConnection())); 57 | } 58 | 59 | [TestMethod] 60 | public async Task CacheCleanup() 61 | { 62 | await AssertCacheCleanupAsync(new MongoDbCacheLayer(MongoDbHelper.GetConnection())); 63 | } 64 | 65 | [TestMethod] 66 | public async Task CachingComplexTypes() 67 | { 68 | await AssertComplexTypeCachingAsync(new MongoDbCacheLayer(MongoDbHelper.GetConnection())); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Providers/FileSystem/FileCacheLayerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | using CacheTower.Providers.FileSystem; 7 | using CacheTower.Serializers.NewtonsoftJson; 8 | using CacheTower.Serializers.Protobuf; 9 | using CacheTower.Serializers.SystemTextJson; 10 | using Microsoft.VisualStudio.TestTools.UnitTesting; 11 | 12 | namespace CacheTower.Tests.Providers.FileSystem 13 | { 14 | [TestClass] 15 | public class FileCacheLayerTests : BaseCacheLayerTests 16 | { 17 | private class TestCacheSerializer : ICacheSerializer 18 | { 19 | public int SerializeCount { get; private set; } 20 | public int DeserializeCount { get; private set; } 21 | 22 | public T Deserialize(Stream stream) 23 | { 24 | DeserializeCount++; 25 | return default; 26 | } 27 | 28 | public void Serialize(Stream stream, T value) 29 | { 30 | SerializeCount++; 31 | } 32 | } 33 | 34 | public const string DirectoryPath = "FileCacheLayer"; 35 | 36 | [TestInitialize] 37 | public void Setup() 38 | { 39 | if (Directory.Exists(DirectoryPath)) 40 | { 41 | Directory.Delete(DirectoryPath, true); 42 | } 43 | } 44 | 45 | [TestMethod] 46 | public async Task CanLoadExistingManifest() 47 | { 48 | var testSerializer = new TestCacheSerializer(); 49 | var cacheLayer = new FileCacheLayer(new(DirectoryPath, testSerializer)); 50 | await using (cacheLayer) 51 | { 52 | //IsAvailableAsync triggers load of manifest which in turn creates it because it doesn't exist 53 | Assert.IsTrue(await cacheLayer.IsAvailableAsync("AnyKey")); 54 | //Disposing will do any other final saves to the manifest 55 | } 56 | Assert.AreEqual(2, testSerializer.SerializeCount); 57 | Assert.AreEqual(0, testSerializer.DeserializeCount); 58 | 59 | testSerializer = new TestCacheSerializer(); 60 | cacheLayer = new FileCacheLayer(new(DirectoryPath, testSerializer)); 61 | await using (cacheLayer) 62 | { 63 | Assert.IsTrue(await cacheLayer.IsAvailableAsync("AnyKey")); 64 | } 65 | Assert.AreEqual(1, testSerializer.SerializeCount); 66 | Assert.AreEqual(1, testSerializer.DeserializeCount); 67 | } 68 | 69 | [TestMethod] 70 | public async Task PersistentGetSetCache() 71 | { 72 | await AssertPersistentGetSetCacheAsync(() => new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance))); 73 | } 74 | 75 | public static IEnumerable GetTestSerializers() 76 | { 77 | yield return new object[] { NewtonsoftJsonCacheSerializer.Instance }; 78 | yield return new object[] { SystemTextJsonCacheSerializer.Instance }; 79 | yield return new object[] { ProtobufCacheSerializer.Instance }; 80 | } 81 | 82 | public static string GetSerializerName(MethodInfo _, object[] values) 83 | { 84 | return values[0].GetType().Name; 85 | } 86 | 87 | [DataTestMethod] 88 | [DynamicData(nameof(GetTestSerializers), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetSerializerName))] 89 | public async Task GetSetCache(ICacheSerializer cacheSerializer) 90 | { 91 | await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, cacheSerializer)); 92 | await AssertGetSetCacheAsync(cacheLayer); 93 | } 94 | 95 | [TestMethod] 96 | public async Task JsonUpgradeTest() 97 | { 98 | { 99 | await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance)); 100 | await cacheLayer.SetAsync("Test", new CacheEntry(new BasicTypeCaching_TypeOne { ExampleString = "JsonUpgradeTest" }, TimeSpan.FromDays(1))); 101 | } 102 | 103 | File.WriteAllText(Path.Combine(DirectoryPath, "0CBC6611F5540BD0809A388DC95A615B"), @"{""Value"":{""ExampleString"":""JsonUpgradeTest""}}"); 104 | 105 | { 106 | await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance)); 107 | var result = await cacheLayer.GetAsync("Test"); 108 | } 109 | } 110 | 111 | [TestMethod] 112 | public async Task IsCacheAvailable() 113 | { 114 | await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance)); 115 | await AssertCacheAvailabilityAsync(cacheLayer, true); 116 | } 117 | 118 | [TestMethod] 119 | public async Task EvictFromCache() 120 | { 121 | await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance)); 122 | await AssertCacheEvictionAsync(cacheLayer); 123 | } 124 | 125 | [TestMethod] 126 | public async Task FlushFromCache() 127 | { 128 | await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance)); 129 | await AssertCacheFlushAsync(cacheLayer); 130 | } 131 | 132 | [TestMethod] 133 | public async Task CacheCleanup() 134 | { 135 | await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, NewtonsoftJsonCacheSerializer.Instance)); 136 | await AssertCacheCleanupAsync(cacheLayer); 137 | } 138 | 139 | [DataTestMethod] 140 | [DynamicData(nameof(GetTestSerializers), DynamicDataSourceType.Method, DynamicDataDisplayName = nameof(GetSerializerName))] 141 | public async Task CachingComplexTypes(ICacheSerializer cacheSerializer) 142 | { 143 | await using var cacheLayer = new FileCacheLayer(new(DirectoryPath, cacheSerializer)); 144 | await AssertComplexTypeCachingAsync(cacheLayer); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Providers/Memory/MemoryCacheLayerTests.cs: -------------------------------------------------------------------------------- 1 | using CacheTower.Providers.Memory; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System.Threading.Tasks; 4 | 5 | namespace CacheTower.Tests.Providers.Memory 6 | { 7 | [TestClass] 8 | public class MemoryCacheLayerTests : BaseCacheLayerTests 9 | { 10 | [TestMethod] 11 | public async Task GetSetCache() 12 | { 13 | await AssertGetSetCacheAsync(new MemoryCacheLayer()); 14 | } 15 | 16 | [TestMethod] 17 | public async Task IsCacheAvailable() 18 | { 19 | await AssertCacheAvailabilityAsync(new MemoryCacheLayer(), true); 20 | } 21 | 22 | [TestMethod] 23 | public async Task EvictFromCache() 24 | { 25 | await AssertCacheEvictionAsync(new MemoryCacheLayer()); 26 | } 27 | 28 | [TestMethod] 29 | public async Task FlushFromCache() 30 | { 31 | await AssertCacheFlushAsync(new MemoryCacheLayer()); 32 | } 33 | 34 | [TestMethod] 35 | public async Task CacheCleanup() 36 | { 37 | await AssertCacheCleanupAsync(new MemoryCacheLayer()); 38 | } 39 | 40 | [TestMethod] 41 | public async Task CachingComplexTypes() 42 | { 43 | await AssertComplexTypeCachingAsync(new MemoryCacheLayer()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Providers/Redis/RedisCacheLayerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CacheTower.Providers.Redis; 4 | using CacheTower.Serializers.Protobuf; 5 | using CacheTower.Tests.Utils; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | using NSubstitute; 8 | using StackExchange.Redis; 9 | 10 | namespace CacheTower.Tests.Providers.Redis 11 | { 12 | [TestClass] 13 | public class RedisCacheLayerTests : BaseCacheLayerTests 14 | { 15 | [TestInitialize] 16 | public void Setup() 17 | { 18 | RedisHelper.ResetState(); 19 | } 20 | 21 | [TestMethod] 22 | public async Task GetSetCache() 23 | { 24 | await AssertGetSetCacheAsync(new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance))); 25 | } 26 | 27 | [TestMethod] 28 | public async Task IsCacheAvailable() 29 | { 30 | await AssertCacheAvailabilityAsync(new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance)), true); 31 | 32 | var connectionMock = Substitute.For(); 33 | var databaseMock = Substitute.For(); 34 | connectionMock.GetDatabase(Arg.Any(), Arg.Any()).Returns(databaseMock); 35 | databaseMock.PingAsync(Arg.Any()).Returns(Task.FromException(new Exception())); 36 | 37 | await AssertCacheAvailabilityAsync(new RedisCacheLayer(connectionMock, new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance)), false); 38 | } 39 | 40 | [TestMethod] 41 | public async Task EvictFromCache() 42 | { 43 | await AssertCacheEvictionAsync(new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance))); 44 | } 45 | 46 | [TestMethod] 47 | public async Task FlushFromCache() 48 | { 49 | await AssertCacheFlushAsync(new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance))); 50 | } 51 | 52 | [TestMethod] 53 | public async Task CacheCleanup() 54 | { 55 | await AssertCacheCleanupAsync(new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance))); 56 | } 57 | 58 | [TestMethod] 59 | public async Task CachingComplexTypes() 60 | { 61 | await AssertComplexTypeCachingAsync(new RedisCacheLayer(RedisHelper.GetConnection(), new RedisCacheLayerOptions(ProtobufCacheSerializer.Instance))); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Serializers/BaseSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using CacheTower.Providers.FileSystem; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | namespace CacheTower.Tests.Serializers 8 | { 9 | public abstract class BaseSerializerTests : TestBase 10 | where TSerializer : ICacheSerializer 11 | { 12 | private static void AssertRoundtripSerialization(TSerializer serializer, TValue value) 13 | where TValue : IEquatable 14 | { 15 | using var memoryStream = new MemoryStream(); 16 | serializer.Serialize(memoryStream, value); 17 | memoryStream.Seek(0, SeekOrigin.Begin); 18 | var result = serializer.Deserialize(memoryStream); 19 | Assert.AreEqual(value, result); 20 | } 21 | 22 | protected static void AssertCacheEntrySerialization(TSerializer serializer) 23 | { 24 | var cacheEntry = new CacheEntry(new ComplexTypeCaching_TypeOne 25 | { 26 | ExampleString = "Hello World", 27 | ExampleNumber = 99, 28 | ListOfNumbers = new List() { 1, 2, 4, 8 } 29 | }, TimeSpan.FromDays(1)); 30 | AssertRoundtripSerialization(serializer, cacheEntry); 31 | } 32 | 33 | protected static void AssertManifestSerialization(TSerializer serializer) 34 | { 35 | var manifest = new ManifestEntry 36 | { 37 | Expiry = new DateTime(2022, 5, 30), 38 | FileName = "CacheEntryFileName" 39 | }; 40 | AssertRoundtripSerialization(serializer, manifest); 41 | } 42 | 43 | protected static void AssertCustomTypeSerialization(TSerializer serializer) 44 | { 45 | AssertRoundtripSerialization(serializer, new ComplexTypeCaching_TypeOne()); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Serializers/NewtonsoftJsonCacheSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using CacheTower.Serializers.NewtonsoftJson; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace CacheTower.Tests.Serializers 5 | { 6 | [TestClass] 7 | public class NewtonsoftJsonCacheSerializerTests : BaseSerializerTests 8 | { 9 | [TestMethod] 10 | public void CacheEntrySerialization() 11 | { 12 | AssertCacheEntrySerialization(NewtonsoftJsonCacheSerializer.Instance); 13 | } 14 | 15 | [TestMethod] 16 | public void ManifestSerialization() 17 | { 18 | AssertManifestSerialization(NewtonsoftJsonCacheSerializer.Instance); 19 | } 20 | 21 | [TestMethod] 22 | public void CustomTypeSerialization() 23 | { 24 | AssertCustomTypeSerialization(NewtonsoftJsonCacheSerializer.Instance); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Serializers/ProtobufCacheSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using CacheTower.Serializers.Protobuf; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace CacheTower.Tests.Serializers 5 | { 6 | [TestClass] 7 | public class ProtobufCacheSerializerTests : BaseSerializerTests 8 | { 9 | [TestMethod] 10 | public void CacheEntrySerialization() 11 | { 12 | AssertCacheEntrySerialization(ProtobufCacheSerializer.Instance); 13 | } 14 | 15 | [TestMethod] 16 | public void ManifestSerialization() 17 | { 18 | AssertManifestSerialization(ProtobufCacheSerializer.Instance); 19 | } 20 | 21 | [TestMethod] 22 | public void CustomTypeSerialization() 23 | { 24 | AssertCustomTypeSerialization(ProtobufCacheSerializer.Instance); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Serializers/SystemTextJsonCacheSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using CacheTower.Serializers.SystemTextJson; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | 4 | namespace CacheTower.Tests.Serializers 5 | { 6 | [TestClass] 7 | public class SystemTextJsonCacheSerializerTests : BaseSerializerTests 8 | { 9 | [TestMethod] 10 | public void CacheEntrySerialization() 11 | { 12 | AssertCacheEntrySerialization(SystemTextJsonCacheSerializer.Instance); 13 | } 14 | 15 | [TestMethod] 16 | public void ManifestSerialization() 17 | { 18 | AssertManifestSerialization(SystemTextJsonCacheSerializer.Instance); 19 | } 20 | 21 | [TestMethod] 22 | public void CustomTypeSerialization() 23 | { 24 | AssertCustomTypeSerialization(SystemTextJsonCacheSerializer.Instance); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/TestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace CacheTower.Tests 8 | { 9 | public abstract class TestBase 10 | { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Utils/MongoDbHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using MongoDB.Driver; 6 | using MongoFramework; 7 | 8 | namespace CacheTower.Tests.Utils 9 | { 10 | public static class MongoDbHelper 11 | { 12 | public static string ConnectionString => Environment.GetEnvironmentVariable("MONGODB_URI") ?? "mongodb://localhost"; 13 | 14 | public static string GetDatabaseName() 15 | { 16 | return "CacheTowerTests"; 17 | } 18 | 19 | public static IMongoDbConnection GetConnection() 20 | { 21 | var urlBuilder = new MongoUrlBuilder(ConnectionString) 22 | { 23 | DatabaseName = GetDatabaseName() 24 | }; 25 | return MongoDbConnection.FromUrl(urlBuilder.ToMongoUrl()); 26 | } 27 | 28 | public static async Task DropDatabaseAsync() 29 | { 30 | await GetConnection().Client.DropDatabaseAsync(GetDatabaseName()); 31 | } 32 | public static void DropDatabase() 33 | { 34 | GetConnection().Client.DropDatabase(GetDatabaseName()); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/CacheTower.Tests/Utils/RedisHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Diagnostics; 4 | using StackExchange.Redis; 5 | 6 | namespace CacheTower.Tests.Utils; 7 | 8 | public static class RedisHelper 9 | { 10 | public static string Endpoint => Environment.GetEnvironmentVariable("REDIS_ENDPOINT") ?? "localhost:6379"; 11 | 12 | private static readonly ConcurrentQueue Errors = new(); 13 | 14 | static RedisHelper() 15 | { 16 | var connection = GetConnection(); 17 | connection.ErrorMessage += (sender, args) => 18 | { 19 | Errors.Enqueue(args.Message); 20 | }; 21 | connection.InternalError += (sender, args) => 22 | { 23 | if (args.Exception is not null) 24 | { 25 | Errors.Enqueue(args.Exception.Message); 26 | } 27 | }; 28 | } 29 | 30 | private static ConnectionMultiplexer Connection { get; set; } 31 | 32 | public static ConnectionMultiplexer GetConnection() 33 | { 34 | if (Connection == null) 35 | { 36 | var config = new ConfigurationOptions 37 | { 38 | AllowAdmin = true 39 | }; 40 | config.EndPoints.Add(Endpoint); 41 | Connection = ConnectionMultiplexer.Connect(config); 42 | } 43 | return Connection; 44 | } 45 | 46 | /// 47 | /// Flushes Redis and resets the state of error logging 48 | /// 49 | public static void ResetState() 50 | { 51 | GetConnection().GetServer(Endpoint).FlushDatabase(); 52 | GetConnection().GetSubscriber().UnsubscribeAll(); 53 | 54 | //.NET Framework doesn't support `Clear()` on Errors so we do it manually 55 | while (!Errors.IsEmpty) 56 | { 57 | Errors.TryDequeue(out _); 58 | } 59 | } 60 | 61 | public static void DebugInfo(IConnectionMultiplexer connection) 62 | { 63 | Debug.WriteLine("== Redis Connection Status =="); 64 | Debug.WriteLine(connection.GetStatus()); 65 | 66 | Debug.WriteLine("== Errors (Redis and Internal) =="); 67 | while (!Errors.IsEmpty) 68 | { 69 | if (Errors.TryDequeue(out var message)) 70 | { 71 | Debug.WriteLine(message); 72 | } 73 | } 74 | } 75 | } 76 | --------------------------------------------------------------------------------