├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ServiceFabricDistributedCache.sln ├── assets ├── icon-128x128.png ├── icon-64x64.png └── microsoft-service-fabric-distributed-cache.png ├── azure-pipelines.yml ├── examples ├── DistributedCache │ ├── ApplicationPackageRoot │ │ └── ApplicationManifest.xml │ ├── ApplicationParameters │ │ ├── Cloud.xml │ │ ├── Local.1Node.xml │ │ └── Local.5Node.xml │ ├── DistributedCache.sfproj │ ├── PublishProfiles │ │ ├── Cloud.xml │ │ ├── Local.1Node.xml │ │ └── Local.5Node.xml │ ├── Scripts │ │ └── Deploy-FabricApplication.ps1 │ └── packages.config ├── LoadTestApp │ ├── LoadTestApp.csproj │ ├── Program.cs │ └── Worker.cs └── Services │ ├── ClientApp │ ├── ClientApp.cs │ ├── ClientApp.csproj │ ├── Controllers │ │ └── CacheDemoController.cs │ ├── PackageRoot │ │ ├── Config │ │ │ └── Settings.xml │ │ └── ServiceManifest.xml │ ├── Program.cs │ ├── ServiceEventSource.cs │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json │ └── DistributedCacheStore │ ├── DistributedCacheStore.cs │ ├── DistributedCacheStore.csproj │ ├── PackageRoot │ ├── Config │ │ └── Settings.xml │ └── ServiceManifest.xml │ ├── Program.cs │ └── ServiceEventSource.cs ├── src └── SoCreate.Extensions.Caching.ServiceFabric │ ├── CacheStoreException.cs │ ├── CacheStoreMetadata.cs │ ├── CacheStoreNotFoundException.cs │ ├── CachedItem.cs │ ├── DistributedCacheStoreLocator.cs │ ├── DistributedCacheStoreService.cs │ ├── IDistributedCacheStoreLocator.cs │ ├── IServiceFabricCacheStoreService.cs │ ├── LinkedDictionaryHelper.cs │ ├── RetryHelper.cs │ ├── ServiceFabricCacheOptions.cs │ ├── ServiceFabricCachingServicesExtensions.cs │ ├── ServiceFabricDistributedCache.cs │ └── SoCreate.Extensions.Caching.ServiceFabric.csproj └── tests └── SoCreate.Extensions.Caching.Tests ├── AutoMoqDataAttribute.cs ├── DistributedCacheStoreServiceTest.cs ├── LinkedDictionaryHelperTest.cs └── SoCreate.Extensions.Caching.Tests.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2.0.0 (2019-12-16) 2 | 3 | 4 | 5 | ### Upgrade 6 | * **Service Fabric** Upgrade Service Fabric to SDK v4. 7 | * **Example** Upgrade example project to .NET Core 3.1. 8 | 9 | # 1.0.7 (2019-03-11) 10 | 11 | 12 | 13 | ### Bug Fixes 14 | * **caching:** Fix issue with getting a cached item not working consistently in the senario where your cache store has multiple partitions 15 | * **caching:** Add better error message when client can't find the cache store in the Service Fabric cluster 16 | 17 | # 1.0.6 (2019-03-06) 18 | 19 | 20 | 21 | ### Bug Fixes 22 | * **caching:** Fix issue with non-expired cached items getting removed from cache if cache is full and cached item was not recently retrieved 23 | * **caching:** Fix issue with cache being cleared when cache store process was restarted 24 | 25 | # 1.0.5 (2019-03-01) 26 | 27 | 28 | 29 | ### Bug Fixes 30 | * **package information:** Update Nuget package information 31 | 32 | # 1.0.4 (2019-02-28) 33 | 34 | 35 | 36 | ### Bug Fixes 37 | * **caching:** Fix issue with Set not keeping cached items expiration settings 38 | * **caching:** Fix issue with cache items set to absolute expiration not being updated as recently retrieved 39 | 40 | # 1.0.1 (2019-02-26) 41 | 42 | ### Bug Fixes 43 | * **package dependencies:** Fix dependencies for Nuget package 44 | 45 | # 1.0.0 (2019-02-26) 46 | 47 | 48 | 49 | ### Initial Release 50 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Service Fabric Distributed Cache 2 | 3 | - [Issues and Bugs](#issue) 4 | - [Request/Add a Feature](#feature) 5 | 6 | ## Found an Issue? 7 | If you find a bug in the source code or a mistake in the documentation, you can help us by 8 | [submitting an issue][submitissue] to our [GitHub Repository][github]. Even better, 9 | you can [submit a Pull Request](#submit-pr) with a fix. 10 | 11 | ## Want a Feature? 12 | You can *request* a new feature by [submitting an issue][submitissue] to our [GitHub 13 | Repository][github]. If you would like to *implement* a new feature, please submit an issue with 14 | a proposal for your work first, to be sure that we can use it. 15 | Please consider what kind of change it is: 16 | 17 | * For a **Major Feature**, first open an issue and outline your proposal so that it can be 18 | discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, 19 | and help you to craft the change so that it is successfully accepted into the project. 20 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 21 | 22 | ### Submitting a Pull Request (PR) 23 | Before you submit your Pull Request (PR) consider the following guidelines: 24 | 25 | * Search [GitHub](https://github.com/socreate/service-fabric-distributed-cache/pulls) for an open or closed PR 26 | that relates to your submission. You don't want to duplicate effort. 27 | * Make your changes in a new git branch: 28 | 29 | ```shell 30 | git checkout -b my-fix-branch master 31 | ``` 32 | 33 | * Create your patch. 34 | * Commit your changes using a descriptive commit message. 35 | * Push your branch to GitHub: 36 | 37 | ```shell 38 | git push origin my-fix-branch 39 | ``` 40 | 41 | * In GitHub, send a pull request to `service-fabric-distributed-cache:master`. 42 | * If we suggest changes then: 43 | * Make the required updates. 44 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 45 | 46 | ```shell 47 | git rebase master -i 48 | git push -f 49 | ``` 50 | 51 | That's it! Thank you for your contribution! 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 SoCreate 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![Service Fabric Distributed Cache](./assets/microsoft-service-fabric-distributed-cache.png)](./assets/microsoft-service-fabric-distributed-cache.png) 2 | 3 | # Service Fabric Distributed Cache 4 | An implementation of the IDistributedCache that uses a Stateful Reliable Service Fabric service to act as the cache store. You can use this library to setup a distributed cache and use Service Fabric instead of Redis or SQL Server. 5 | 6 | [![Build Status](https://dev.azure.com/SoCreate/Open%20Source%20Projects/_apis/build/status/SoCreate.service-fabric-distributed-cache?branchName=master)](https://dev.azure.com/SoCreate/Open%20Source%20Projects/_build/latest?definitionId=17&branchName=master) 7 | [![NuGet Badge](https://buildstats.info/nuget/SoCreate.Extensions.Caching.ServiceFabric)](https://www.nuget.org/packages/SoCreate.Extensions.Caching.ServiceFabric/) 8 | 9 | ## Documentation () 10 | 11 | ## Contributing 12 | 13 | Help Service Fabric Distributed Cache by contributing! 14 | 15 | ### [Contributing Guide](./CONTRIBUTING.md) 16 | 17 | Please read our [contributing guide](./CONTRIBUTING.md) to learn about filing issues, submitting PRs, and building 18 | Service Fabric Distributed Cache. 19 | 20 | ### License 21 | 22 | Service Fabric Distributed Cache is [MIT licensed](./LICENSE). 23 | 24 | ## Latest Changes 25 | 26 | [Service Fabric Distributed Cache Changelog](./CHANGELOG.md) 27 | -------------------------------------------------------------------------------- /ServiceFabricDistributedCache.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29519.181 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{A07B5EB6-E848-4116-A8D0-A826331D98C6}") = "DistributedCache", "examples\DistributedCache\DistributedCache.sfproj", "{083FA0A4-F720-4F9E-97D1-77EB86A51F79}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ClientApp", "examples\Services\ClientApp\ClientApp.csproj", "{0B98E889-AC0D-4772-A7E5-BDCC204F1A17}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistributedCacheStore", "examples\Services\DistributedCacheStore\DistributedCacheStore.csproj", "{78084B43-5F67-4F8E-BDE2-4D788A9C9B09}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SoCreate.Extensions.Caching.ServiceFabric", "src\SoCreate.Extensions.Caching.ServiceFabric\SoCreate.Extensions.Caching.ServiceFabric.csproj", "{2CD13B96-A04B-4886-A32F-646DF237820E}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SoCreate.Extensions.Caching.Tests", "tests\SoCreate.Extensions.Caching.Tests\SoCreate.Extensions.Caching.Tests.csproj", "{E5B6BE91-0114-4C98-8AEA-96C5A7E5026E}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{7F2A2566-D52A-49FD-AB75-C1145259F5F1}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoadTestApp", "examples\LoadTestApp\LoadTestApp.csproj", "{4FDBA5FD-14B5-4B03-85C6-3685CBE610D3}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{03079911-D689-4300-80F5-84040518D04C}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BB3EDFF6-4B21-4562-8B9B-3C36A8089EF3}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Debug|x64 = Debug|x64 28 | Release|Any CPU = Release|Any CPU 29 | Release|x64 = Release|x64 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79}.Debug|Any CPU.ActiveCfg = Debug|x64 33 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79}.Debug|Any CPU.Build.0 = Debug|x64 34 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79}.Debug|Any CPU.Deploy.0 = Debug|x64 35 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79}.Debug|x64.ActiveCfg = Debug|x64 36 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79}.Debug|x64.Build.0 = Debug|x64 37 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79}.Debug|x64.Deploy.0 = Debug|x64 38 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79}.Release|Any CPU.ActiveCfg = Release|x64 39 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79}.Release|x64.ActiveCfg = Release|x64 40 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79}.Release|x64.Build.0 = Release|x64 41 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79}.Release|x64.Deploy.0 = Release|x64 42 | {0B98E889-AC0D-4772-A7E5-BDCC204F1A17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {0B98E889-AC0D-4772-A7E5-BDCC204F1A17}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {0B98E889-AC0D-4772-A7E5-BDCC204F1A17}.Debug|x64.ActiveCfg = Debug|Any CPU 45 | {0B98E889-AC0D-4772-A7E5-BDCC204F1A17}.Debug|x64.Build.0 = Debug|Any CPU 46 | {0B98E889-AC0D-4772-A7E5-BDCC204F1A17}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {0B98E889-AC0D-4772-A7E5-BDCC204F1A17}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {0B98E889-AC0D-4772-A7E5-BDCC204F1A17}.Release|x64.ActiveCfg = Release|Any CPU 49 | {0B98E889-AC0D-4772-A7E5-BDCC204F1A17}.Release|x64.Build.0 = Release|Any CPU 50 | {78084B43-5F67-4F8E-BDE2-4D788A9C9B09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {78084B43-5F67-4F8E-BDE2-4D788A9C9B09}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {78084B43-5F67-4F8E-BDE2-4D788A9C9B09}.Debug|x64.ActiveCfg = Debug|Any CPU 53 | {78084B43-5F67-4F8E-BDE2-4D788A9C9B09}.Debug|x64.Build.0 = Debug|Any CPU 54 | {78084B43-5F67-4F8E-BDE2-4D788A9C9B09}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {78084B43-5F67-4F8E-BDE2-4D788A9C9B09}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {78084B43-5F67-4F8E-BDE2-4D788A9C9B09}.Release|x64.ActiveCfg = Release|Any CPU 57 | {78084B43-5F67-4F8E-BDE2-4D788A9C9B09}.Release|x64.Build.0 = Release|Any CPU 58 | {2CD13B96-A04B-4886-A32F-646DF237820E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {2CD13B96-A04B-4886-A32F-646DF237820E}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {2CD13B96-A04B-4886-A32F-646DF237820E}.Debug|x64.ActiveCfg = Debug|Any CPU 61 | {2CD13B96-A04B-4886-A32F-646DF237820E}.Debug|x64.Build.0 = Debug|Any CPU 62 | {2CD13B96-A04B-4886-A32F-646DF237820E}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {2CD13B96-A04B-4886-A32F-646DF237820E}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {2CD13B96-A04B-4886-A32F-646DF237820E}.Release|x64.ActiveCfg = Release|Any CPU 65 | {2CD13B96-A04B-4886-A32F-646DF237820E}.Release|x64.Build.0 = Release|Any CPU 66 | {E5B6BE91-0114-4C98-8AEA-96C5A7E5026E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {E5B6BE91-0114-4C98-8AEA-96C5A7E5026E}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {E5B6BE91-0114-4C98-8AEA-96C5A7E5026E}.Debug|x64.ActiveCfg = Debug|Any CPU 69 | {E5B6BE91-0114-4C98-8AEA-96C5A7E5026E}.Debug|x64.Build.0 = Debug|Any CPU 70 | {E5B6BE91-0114-4C98-8AEA-96C5A7E5026E}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {E5B6BE91-0114-4C98-8AEA-96C5A7E5026E}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {E5B6BE91-0114-4C98-8AEA-96C5A7E5026E}.Release|x64.ActiveCfg = Release|Any CPU 73 | {E5B6BE91-0114-4C98-8AEA-96C5A7E5026E}.Release|x64.Build.0 = Release|Any CPU 74 | {4FDBA5FD-14B5-4B03-85C6-3685CBE610D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 75 | {4FDBA5FD-14B5-4B03-85C6-3685CBE610D3}.Debug|Any CPU.Build.0 = Debug|Any CPU 76 | {4FDBA5FD-14B5-4B03-85C6-3685CBE610D3}.Debug|x64.ActiveCfg = Debug|Any CPU 77 | {4FDBA5FD-14B5-4B03-85C6-3685CBE610D3}.Debug|x64.Build.0 = Debug|Any CPU 78 | {4FDBA5FD-14B5-4B03-85C6-3685CBE610D3}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {4FDBA5FD-14B5-4B03-85C6-3685CBE610D3}.Release|Any CPU.Build.0 = Release|Any CPU 80 | {4FDBA5FD-14B5-4B03-85C6-3685CBE610D3}.Release|x64.ActiveCfg = Release|Any CPU 81 | {4FDBA5FD-14B5-4B03-85C6-3685CBE610D3}.Release|x64.Build.0 = Release|Any CPU 82 | EndGlobalSection 83 | GlobalSection(SolutionProperties) = preSolution 84 | HideSolutionNode = FALSE 85 | EndGlobalSection 86 | GlobalSection(NestedProjects) = preSolution 87 | {083FA0A4-F720-4F9E-97D1-77EB86A51F79} = {7F2A2566-D52A-49FD-AB75-C1145259F5F1} 88 | {0B98E889-AC0D-4772-A7E5-BDCC204F1A17} = {7F2A2566-D52A-49FD-AB75-C1145259F5F1} 89 | {78084B43-5F67-4F8E-BDE2-4D788A9C9B09} = {7F2A2566-D52A-49FD-AB75-C1145259F5F1} 90 | {2CD13B96-A04B-4886-A32F-646DF237820E} = {03079911-D689-4300-80F5-84040518D04C} 91 | {E5B6BE91-0114-4C98-8AEA-96C5A7E5026E} = {BB3EDFF6-4B21-4562-8B9B-3C36A8089EF3} 92 | {4FDBA5FD-14B5-4B03-85C6-3685CBE610D3} = {7F2A2566-D52A-49FD-AB75-C1145259F5F1} 93 | EndGlobalSection 94 | GlobalSection(ExtensibilityGlobals) = postSolution 95 | SolutionGuid = {CC598FD8-CADC-41C7-B6C1-B9BAB7A6496F} 96 | EndGlobalSection 97 | EndGlobal 98 | -------------------------------------------------------------------------------- /assets/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoCreate/service-fabric-distributed-cache/3d6a925ee0fa4f10105cf43b0d05caab88614b48/assets/icon-128x128.png -------------------------------------------------------------------------------- /assets/icon-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoCreate/service-fabric-distributed-cache/3d6a925ee0fa4f10105cf43b0d05caab88614b48/assets/icon-64x64.png -------------------------------------------------------------------------------- /assets/microsoft-service-fabric-distributed-cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SoCreate/service-fabric-distributed-cache/3d6a925ee0fa4f10105cf43b0d05caab88614b48/assets/microsoft-service-fabric-distributed-cache.png -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | pr: none 4 | pool: 5 | vmImage: ubuntu-latest 6 | variables: 7 | buildConfiguration: Release 8 | projectPaths: src/**/*.csproj 9 | testProjectPaths: 'tests/**/*[Tt]ests/*.csproj' 10 | steps: 11 | - task: UseDotNet@2 12 | displayName: 'Install .NET 6' 13 | inputs: 14 | version: 6.0.x 15 | - task: DotNetCoreCLI@2 16 | displayName: Test 17 | inputs: 18 | command: test 19 | projects: $(testProjectPaths) 20 | arguments: '--configuration $(BuildConfiguration)' 21 | - task: DotNetCoreCLI@2 22 | displayName: Restore 23 | inputs: 24 | command: restore 25 | projects: $(projectPaths) 26 | - task: DotNetCoreCLI@2 27 | displayName: Build 28 | inputs: 29 | projects: $(projectPaths) 30 | arguments: '--configuration $(buildConfiguration)' 31 | - task: DotNetCoreCLI@2 32 | displayName: Pack 33 | inputs: 34 | command: pack 35 | packagesToPack: $(projectPaths) 36 | nobuild: true 37 | - task: NuGetToolInstaller@0 38 | displayName: Use NuGet 4.9.3 39 | inputs: 40 | versionSpec: 4.9.3 41 | - task: NuGetCommand@2 42 | displayName: NuGet push 43 | inputs: 44 | command: push 45 | nuGetFeedType: external 46 | publishFeedCredentials: Public Nuget 47 | - task: PublishBuildArtifacts@1 48 | displayName: Publish Artifact 49 | inputs: 50 | PathtoPublish: $(build.artifactstagingdirectory) 51 | -------------------------------------------------------------------------------- /examples/DistributedCache/ApplicationPackageRoot/ApplicationManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/DistributedCache/ApplicationParameters/Cloud.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/DistributedCache/ApplicationParameters/Local.1Node.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/DistributedCache/ApplicationParameters/Local.5Node.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/DistributedCache/DistributedCache.sfproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 083fa0a4-f720-4f9e-97d1-77eb86a51f79 6 | 2.4 7 | 1.5 8 | 1.7.1 9 | v4.7.2 10 | 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Service Fabric Tools\Microsoft.VisualStudio.Azure.Fabric.ApplicationProject.targets 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/DistributedCache/PublishProfiles/Cloud.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /examples/DistributedCache/PublishProfiles/Local.1Node.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/DistributedCache/PublishProfiles/Local.5Node.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/DistributedCache/Scripts/Deploy-FabricApplication.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Deploys a Service Fabric application type to a cluster. 4 | 5 | .DESCRIPTION 6 | This script deploys a Service Fabric application type to a cluster. It is invoked by Visual Studio when deploying a Service Fabric Application project. 7 | 8 | .NOTES 9 | WARNING: This script file is invoked by Visual Studio. Its parameters must not be altered but its logic can be customized as necessary. 10 | 11 | .PARAMETER PublishProfileFile 12 | Path to the file containing the publish profile. 13 | 14 | .PARAMETER ApplicationPackagePath 15 | Path to the folder of the packaged Service Fabric application. 16 | 17 | .PARAMETER DeployOnly 18 | Indicates that the Service Fabric application should not be created or upgraded after registering the application type. 19 | 20 | .PARAMETER ApplicationParameter 21 | Hashtable of the Service Fabric application parameters to be used for the application. 22 | 23 | .PARAMETER UnregisterUnusedApplicationVersionsAfterUpgrade 24 | Indicates whether to unregister any unused application versions that exist after an upgrade is finished. 25 | 26 | .PARAMETER OverrideUpgradeBehavior 27 | Indicates the behavior used to override the upgrade settings specified by the publish profile. 28 | 'None' indicates that the upgrade settings will not be overridden. 29 | 'ForceUpgrade' indicates that an upgrade will occur with default settings, regardless of what is specified in the publish profile. 30 | 'VetoUpgrade' indicates that an upgrade will not occur, regardless of what is specified in the publish profile. 31 | 32 | .PARAMETER UseExistingClusterConnection 33 | Indicates that the script should make use of an existing cluster connection that has already been established in the PowerShell session. The cluster connection parameters configured in the publish profile are ignored. 34 | 35 | .PARAMETER OverwriteBehavior 36 | Overwrite Behavior if an application exists in the cluster with the same name. Available Options are Never, Always, SameAppTypeAndVersion. This setting is not applicable when upgrading an application. 37 | 'Never' will not remove the existing application. This is the default behavior. 38 | 'Always' will remove the existing application even if its Application type and Version is different from the application being created. 39 | 'SameAppTypeAndVersion' will remove the existing application only if its Application type and Version is same as the application being created. 40 | 41 | .PARAMETER SkipPackageValidation 42 | Switch signaling whether the package should be validated or not before deployment. 43 | 44 | .PARAMETER SecurityToken 45 | A security token for authentication to cluster management endpoints. Used for silent authentication to clusters that are protected by Azure Active Directory. 46 | 47 | .PARAMETER CopyPackageTimeoutSec 48 | Timeout in seconds for copying application package to image store. 49 | 50 | .EXAMPLE 51 | . Scripts\Deploy-FabricApplication.ps1 -ApplicationPackagePath 'pkg\Debug' 52 | 53 | Deploy the application using the default package location for a Debug build. 54 | 55 | .EXAMPLE 56 | . Scripts\Deploy-FabricApplication.ps1 -ApplicationPackagePath 'pkg\Debug' -DoNotCreateApplication 57 | 58 | Deploy the application but do not create the application instance. 59 | 60 | .EXAMPLE 61 | . Scripts\Deploy-FabricApplication.ps1 -ApplicationPackagePath 'pkg\Debug' -ApplicationParameter @{CustomParameter1='MyValue'; CustomParameter2='MyValue'} 62 | 63 | Deploy the application by providing values for parameters that are defined in the application manifest. 64 | #> 65 | 66 | Param 67 | ( 68 | [String] 69 | $PublishProfileFile, 70 | 71 | [String] 72 | $ApplicationPackagePath, 73 | 74 | [Switch] 75 | $DeployOnly, 76 | 77 | [Hashtable] 78 | $ApplicationParameter, 79 | 80 | [Boolean] 81 | $UnregisterUnusedApplicationVersionsAfterUpgrade, 82 | 83 | [String] 84 | [ValidateSet('None', 'ForceUpgrade', 'VetoUpgrade')] 85 | $OverrideUpgradeBehavior = 'None', 86 | 87 | [Switch] 88 | $UseExistingClusterConnection, 89 | 90 | [String] 91 | [ValidateSet('Never','Always','SameAppTypeAndVersion')] 92 | $OverwriteBehavior = 'Never', 93 | 94 | [Switch] 95 | $SkipPackageValidation, 96 | 97 | [String] 98 | $SecurityToken, 99 | 100 | [int] 101 | $CopyPackageTimeoutSec, 102 | 103 | [int] 104 | $RegisterApplicationTypeTimeoutSec 105 | ) 106 | 107 | function Read-XmlElementAsHashtable 108 | { 109 | Param ( 110 | [System.Xml.XmlElement] 111 | $Element 112 | ) 113 | 114 | $hashtable = @{} 115 | if ($Element.Attributes) 116 | { 117 | $Element.Attributes | 118 | ForEach-Object { 119 | $boolVal = $null 120 | if ([bool]::TryParse($_.Value, [ref]$boolVal)) { 121 | $hashtable[$_.Name] = $boolVal 122 | } 123 | else { 124 | $hashtable[$_.Name] = $_.Value 125 | } 126 | } 127 | } 128 | 129 | return $hashtable 130 | } 131 | 132 | function Read-PublishProfile 133 | { 134 | Param ( 135 | [ValidateScript({Test-Path $_ -PathType Leaf})] 136 | [String] 137 | $PublishProfileFile 138 | ) 139 | 140 | $publishProfileXml = [Xml] (Get-Content $PublishProfileFile) 141 | $publishProfile = @{} 142 | 143 | $publishProfile.ClusterConnectionParameters = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("ClusterConnectionParameters") 144 | $publishProfile.UpgradeDeployment = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("UpgradeDeployment") 145 | $publishProfile.CopyPackageParameters = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("CopyPackageParameters") 146 | $publishProfile.RegisterApplicationParameters = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("RegisterApplicationParameters") 147 | 148 | if ($publishProfileXml.PublishProfile.Item("UpgradeDeployment")) 149 | { 150 | $publishProfile.UpgradeDeployment.Parameters = Read-XmlElementAsHashtable $publishProfileXml.PublishProfile.Item("UpgradeDeployment").Item("Parameters") 151 | if ($publishProfile.UpgradeDeployment["Mode"]) 152 | { 153 | $publishProfile.UpgradeDeployment.Parameters[$publishProfile.UpgradeDeployment["Mode"]] = $true 154 | } 155 | } 156 | 157 | $publishProfileFolder = (Split-Path $PublishProfileFile) 158 | $publishProfile.ApplicationParameterFile = [System.IO.Path]::Combine($PublishProfileFolder, $publishProfileXml.PublishProfile.ApplicationParameterFile.Path) 159 | 160 | return $publishProfile 161 | } 162 | 163 | $LocalFolder = (Split-Path $MyInvocation.MyCommand.Path) 164 | 165 | if (!$PublishProfileFile) 166 | { 167 | $PublishProfileFile = "$LocalFolder\..\PublishProfiles\Local.xml" 168 | } 169 | 170 | if (!$ApplicationPackagePath) 171 | { 172 | $ApplicationPackagePath = "$LocalFolder\..\pkg\Release" 173 | } 174 | 175 | $ApplicationPackagePath = Resolve-Path $ApplicationPackagePath 176 | 177 | $publishProfile = Read-PublishProfile $PublishProfileFile 178 | 179 | if (-not $UseExistingClusterConnection) 180 | { 181 | $ClusterConnectionParameters = $publishProfile.ClusterConnectionParameters 182 | if ($SecurityToken) 183 | { 184 | $ClusterConnectionParameters["SecurityToken"] = $SecurityToken 185 | } 186 | 187 | try 188 | { 189 | [void](Connect-ServiceFabricCluster @ClusterConnectionParameters) 190 | } 191 | catch [System.Fabric.FabricObjectClosedException] 192 | { 193 | Write-Warning "Service Fabric cluster may not be connected." 194 | throw 195 | } 196 | } 197 | 198 | $RegKey = "HKLM:\SOFTWARE\Microsoft\Service Fabric SDK" 199 | $ModuleFolderPath = (Get-ItemProperty -Path $RegKey -Name FabricSDKPSModulePath).FabricSDKPSModulePath 200 | Import-Module "$ModuleFolderPath\ServiceFabricSDK.psm1" 201 | 202 | $IsUpgrade = ($publishProfile.UpgradeDeployment -and $publishProfile.UpgradeDeployment.Enabled -and $OverrideUpgradeBehavior -ne 'VetoUpgrade') -or $OverrideUpgradeBehavior -eq 'ForceUpgrade' 203 | 204 | $PublishParameters = @{ 205 | 'ApplicationPackagePath' = $ApplicationPackagePath 206 | 'ApplicationParameterFilePath' = $publishProfile.ApplicationParameterFile 207 | 'ApplicationParameter' = $ApplicationParameter 208 | 'ErrorAction' = 'Stop' 209 | } 210 | 211 | if ($publishProfile.CopyPackageParameters.CopyPackageTimeoutSec) 212 | { 213 | $PublishParameters['CopyPackageTimeoutSec'] = $publishProfile.CopyPackageParameters.CopyPackageTimeoutSec 214 | } 215 | 216 | if ($publishProfile.CopyPackageParameters.CompressPackage) 217 | { 218 | $PublishParameters['CompressPackage'] = $publishProfile.CopyPackageParameters.CompressPackage 219 | } 220 | 221 | if ($publishProfile.RegisterApplicationParameters.RegisterApplicationTypeTimeoutSec) 222 | { 223 | $PublishParameters['RegisterApplicationTypeTimeoutSec'] = $publishProfile.RegisterApplicationParameters.RegisterApplicationTypeTimeoutSec 224 | } 225 | 226 | # CopyPackageTimeoutSec parameter overrides the value from the publish profile 227 | if ($CopyPackageTimeoutSec) 228 | { 229 | $PublishParameters['CopyPackageTimeoutSec'] = $CopyPackageTimeoutSec 230 | } 231 | 232 | # RegisterApplicationTypeTimeoutSec parameter overrides the value from the publish profile 233 | if ($RegisterApplicationTypeTimeoutSec) 234 | { 235 | $PublishParameters['RegisterApplicationTypeTimeoutSec'] = $RegisterApplicationTypeTimeoutSec 236 | } 237 | 238 | if ($IsUpgrade) 239 | { 240 | $Action = "RegisterAndUpgrade" 241 | if ($DeployOnly) 242 | { 243 | $Action = "Register" 244 | } 245 | 246 | $UpgradeParameters = $publishProfile.UpgradeDeployment.Parameters 247 | 248 | if ($OverrideUpgradeBehavior -eq 'ForceUpgrade') 249 | { 250 | # Warning: Do not alter these upgrade parameters. It will create an inconsistency with Visual Studio's behavior. 251 | $UpgradeParameters = @{ UnmonitoredAuto = $true; Force = $true } 252 | } 253 | 254 | $PublishParameters['Action'] = $Action 255 | $PublishParameters['UpgradeParameters'] = $UpgradeParameters 256 | $PublishParameters['UnregisterUnusedVersions'] = $UnregisterUnusedApplicationVersionsAfterUpgrade 257 | 258 | Publish-UpgradedServiceFabricApplication @PublishParameters 259 | } 260 | else 261 | { 262 | $Action = "RegisterAndCreate" 263 | if ($DeployOnly) 264 | { 265 | $Action = "Register" 266 | } 267 | 268 | $PublishParameters['Action'] = $Action 269 | $PublishParameters['OverwriteBehavior'] = $OverwriteBehavior 270 | $PublishParameters['SkipPackageValidation'] = $SkipPackageValidation 271 | 272 | Publish-NewServiceFabricApplication @PublishParameters 273 | } -------------------------------------------------------------------------------- /examples/DistributedCache/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /examples/LoadTestApp/LoadTestApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/LoadTestApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Hosting; 3 | using System; 4 | 5 | namespace LoadTestApp 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureServices((hostContext, services) => 17 | { 18 | services.AddDistributedServiceFabricCache(options => options.RetryTimeout = TimeSpan.FromSeconds(5)); 19 | services.AddHostedService(); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/LoadTestApp/Worker.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Distributed; 2 | using Microsoft.Extensions.Hosting; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace LoadTestApp 11 | { 12 | class Worker : IHostedService 13 | { 14 | private readonly IDistributedCache _distributedCache; 15 | private Stopwatch _stopWatch = new Stopwatch(); 16 | private List _timeMeasures = new List(); 17 | 18 | public Worker(IDistributedCache distributedCache) 19 | { 20 | _distributedCache = distributedCache; 21 | } 22 | public async Task StartAsync(CancellationToken cancellationToken) 23 | { 24 | byte[] bytes = Encoding.ASCII.GetBytes("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum"); 25 | var options = new DistributedCacheEntryOptions(); 26 | options.SlidingExpiration = TimeSpan.FromSeconds(20); 27 | 28 | var tasks = new List>(); 29 | 30 | for (var i = 0; i < 5000; i++) 31 | { 32 | cancellationToken.ThrowIfCancellationRequested(); 33 | _timeMeasures.Add(MeasureTime(() => { 34 | _distributedCache.Set($"SetKey2-{i}", bytes, options); 35 | }, "SetKey")); 36 | } 37 | 38 | for (var i = 0; i < 5000; i++) 39 | { 40 | cancellationToken.ThrowIfCancellationRequested(); 41 | _timeMeasures.Add(await MeasureTimeAsync(async () => { 42 | await _distributedCache.SetAsync($"SetAsyncAwaitedKey2-{i}", bytes, options, cancellationToken); 43 | }, "SetAsyncAwaitedKey")); 44 | } 45 | 46 | for (var i = 0; i < 5000; i++) 47 | { 48 | cancellationToken.ThrowIfCancellationRequested(); 49 | tasks.Add(MeasureTimeAsync(async () => { 50 | await _distributedCache.SetAsync($"SetAsyncAwaitedWhenAllKey2-{i}", bytes, options, cancellationToken); 51 | }, "SetAsyncAwaitedWhenAllKey")); 52 | } 53 | _timeMeasures.AddRange(await Task.WhenAll(tasks)); 54 | 55 | foreach (var timeMeasure in _timeMeasures) 56 | { 57 | cancellationToken.ThrowIfCancellationRequested(); 58 | Console.WriteLine(timeMeasure); 59 | } 60 | 61 | Console.WriteLine("Done!"); 62 | Console.ReadKey(); 63 | } 64 | 65 | public Task StopAsync(CancellationToken cancellationToken) 66 | { 67 | return Task.CompletedTask; 68 | } 69 | 70 | private async Task MeasureTimeAsync(Func action, string label) 71 | { 72 | var stopWatch = new Stopwatch(); 73 | stopWatch.Start(); 74 | await action(); 75 | _stopWatch.Stop(); 76 | return $"{label} {stopWatch.ElapsedMilliseconds}"; 77 | } 78 | 79 | private string MeasureTime(Action action, string label) 80 | { 81 | var stopWatch = new Stopwatch(); 82 | stopWatch.Start(); 83 | action(); 84 | _stopWatch.Stop(); 85 | return $"{label} {stopWatch.ElapsedMilliseconds}"; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/Services/ClientApp/ClientApp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Fabric; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.ServiceFabric.Services.Communication.AspNetCore; 11 | using Microsoft.ServiceFabric.Services.Communication.Runtime; 12 | using Microsoft.ServiceFabric.Services.Runtime; 13 | using Microsoft.ServiceFabric.Data; 14 | 15 | namespace ClientApp 16 | { 17 | /// 18 | /// The FabricRuntime creates an instance of this class for each service type instance. 19 | /// 20 | internal sealed class ClientApp : StatelessService 21 | { 22 | public ClientApp(StatelessServiceContext context) 23 | : base(context) 24 | { } 25 | 26 | /// 27 | /// Optional override to create listeners (like tcp, http) for this service instance. 28 | /// 29 | /// The collection of listeners. 30 | protected override IEnumerable CreateServiceInstanceListeners() 31 | { 32 | return new ServiceInstanceListener[] 33 | { 34 | new ServiceInstanceListener(serviceContext => 35 | new KestrelCommunicationListener(serviceContext, "ServiceEndpoint", (url, listener) => 36 | { 37 | ServiceEventSource.Current.ServiceMessage(serviceContext, $"Starting Kestrel on {url}"); 38 | 39 | return new WebHostBuilder() 40 | .UseKestrel() 41 | .ConfigureServices( 42 | services => services 43 | .AddSingleton(serviceContext)) 44 | .UseContentRoot(Directory.GetCurrentDirectory()) 45 | .UseStartup() 46 | .UseServiceFabricIntegration(listener, ServiceFabricIntegrationOptions.None) 47 | .UseUrls(url) 48 | .Build(); 49 | })) 50 | }; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /examples/Services/ClientApp/ClientApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | InProcess 6 | True 7 | True 8 | win7-x64 9 | False 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/Services/ClientApp/Controllers/CacheDemoController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Caching.Distributed; 8 | 9 | namespace ClientApp.Controllers 10 | { 11 | [Route("api/[controller]")] 12 | [ApiController] 13 | public class CacheDemoController : ControllerBase 14 | { 15 | private readonly IDistributedCache _distributedCache; 16 | 17 | public CacheDemoController(IDistributedCache distributedCache) 18 | { 19 | _distributedCache = distributedCache; 20 | } 21 | 22 | [HttpGet("SetSlidingCacheItem")] 23 | public async Task> SetSlidingCacheItem() 24 | { 25 | var options = new DistributedCacheEntryOptions(); 26 | options.SlidingExpiration = TimeSpan.FromSeconds(20); 27 | 28 | await _distributedCache.SetAsync("SlidingCacheItem", Encoding.UTF8.GetBytes(DateTime.Now.ToString()), options); 29 | 30 | return new EmptyResult(); 31 | } 32 | 33 | [HttpGet("GetSlidingCacheItem")] 34 | public async Task> GetSlidingCacheItem() 35 | { 36 | var bytes = await _distributedCache.GetAsync("SlidingCacheItem"); 37 | 38 | if (bytes != null) 39 | return Content(Encoding.UTF8.GetString(bytes)); 40 | 41 | return new EmptyResult(); 42 | } 43 | 44 | [HttpGet("SetAbsoluteExpirationCacheItem")] 45 | public async Task> SetAbsoluteExpirationCacheItem() 46 | { 47 | var options = new DistributedCacheEntryOptions(); 48 | options.AbsoluteExpiration = DateTime.Now.AddSeconds(20); 49 | 50 | await _distributedCache.SetAsync("AbsoluteExpirationCacheItem", Encoding.UTF8.GetBytes(DateTime.Now.ToString()), options); 51 | 52 | return new EmptyResult(); 53 | } 54 | 55 | [HttpGet("GetAbsoluteExpirationCacheItem")] 56 | public async Task> GetAbsoluteExpirationCacheItem() 57 | { 58 | var bytes = await _distributedCache.GetAsync("AbsoluteExpirationCacheItem"); 59 | 60 | if (bytes != null) 61 | return Content(Encoding.UTF8.GetString(bytes)); 62 | 63 | return new EmptyResult(); 64 | } 65 | 66 | [HttpGet("{key}")] 67 | public async Task> Get(string key) 68 | { 69 | var bytes = await _distributedCache.GetAsync(key); 70 | 71 | if(bytes != null) 72 | return Content(Encoding.UTF8.GetString(bytes)); 73 | 74 | return new EmptyResult(); 75 | } 76 | 77 | [HttpPut("{key}")] 78 | public async Task Put(string key) 79 | { 80 | var request = HttpContext.Request; 81 | using (var reader = new StreamReader(request.Body)) 82 | { 83 | var content = await reader.ReadToEndAsync(); 84 | 85 | var options = new DistributedCacheEntryOptions(); 86 | options.SlidingExpiration = TimeSpan.FromDays(1); 87 | await _distributedCache.SetAsync(key, Encoding.UTF8.GetBytes(content), options); 88 | } 89 | } 90 | 91 | [HttpDelete("{key}")] 92 | public async Task Delete(string key) 93 | { 94 | await _distributedCache.RemoveAsync(key); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /examples/Services/ClientApp/PackageRoot/Config/Settings.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /examples/Services/ClientApp/PackageRoot/ServiceManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ClientApp.exe 18 | CodePackage 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /examples/Services/ClientApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ServiceFabric.Services.Runtime; 2 | using System; 3 | using System.Diagnostics; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace ClientApp 8 | { 9 | internal static class Program 10 | { 11 | /// 12 | /// This is the entry point of the service host process. 13 | /// 14 | private static void Main() 15 | { 16 | try 17 | { 18 | // The ServiceManifest.XML file defines one or more service type names. 19 | // Registering a service maps a service type name to a .NET type. 20 | // When Service Fabric creates an instance of this service type, 21 | // an instance of the class is created in this host process. 22 | 23 | ServiceRuntime.RegisterServiceAsync("ClientAppType", 24 | context => new ClientApp(context)).GetAwaiter().GetResult(); 25 | 26 | ServiceEventSource.Current.ServiceTypeRegistered(Process.GetCurrentProcess().Id, typeof(ClientApp).Name); 27 | 28 | // Prevents this host process from terminating so services keeps running. 29 | Thread.Sleep(Timeout.Infinite); 30 | } 31 | catch (Exception e) 32 | { 33 | ServiceEventSource.Current.ServiceHostInitializationFailed(e.ToString()); 34 | throw; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/Services/ClientApp/ServiceEventSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Tracing; 4 | using System.Fabric; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Microsoft.ServiceFabric.Services.Runtime; 9 | 10 | namespace ClientApp 11 | { 12 | [EventSource(Name = "MyCompany-DistributedCache-ClientApp")] 13 | internal sealed class ServiceEventSource : EventSource 14 | { 15 | public static readonly ServiceEventSource Current = new ServiceEventSource(); 16 | 17 | static ServiceEventSource() 18 | { 19 | // A workaround for the problem where ETW activities do not get tracked until Tasks infrastructure is initialized. 20 | // This problem will be fixed in .NET Framework 4.6.2. 21 | Task.Run(() => { }); 22 | } 23 | 24 | // Instance constructor is private to enforce singleton semantics 25 | private ServiceEventSource() : base() { } 26 | 27 | #region Keywords 28 | // Event keywords can be used to categorize events. 29 | // Each keyword is a bit flag. A single event can be associated with multiple keywords (via EventAttribute.Keywords property). 30 | // Keywords must be defined as a public class named 'Keywords' inside EventSource that uses them. 31 | public static class Keywords 32 | { 33 | public const EventKeywords Requests = (EventKeywords)0x1L; 34 | public const EventKeywords ServiceInitialization = (EventKeywords)0x2L; 35 | } 36 | #endregion 37 | 38 | #region Events 39 | // Define an instance method for each event you want to record and apply an [Event] attribute to it. 40 | // The method name is the name of the event. 41 | // Pass any parameters you want to record with the event (only primitive integer types, DateTime, Guid & string are allowed). 42 | // Each event method implementation should check whether the event source is enabled, and if it is, call WriteEvent() method to raise the event. 43 | // The number and types of arguments passed to every event method must exactly match what is passed to WriteEvent(). 44 | // Put [NonEvent] attribute on all methods that do not define an event. 45 | // For more information see https://msdn.microsoft.com/en-us/library/system.diagnostics.tracing.eventsource.aspx 46 | 47 | [NonEvent] 48 | public void Message(string message, params object[] args) 49 | { 50 | if (this.IsEnabled()) 51 | { 52 | string finalMessage = string.Format(message, args); 53 | Message(finalMessage); 54 | } 55 | } 56 | 57 | private const int MessageEventId = 1; 58 | [Event(MessageEventId, Level = EventLevel.Informational, Message = "{0}")] 59 | public void Message(string message) 60 | { 61 | if (this.IsEnabled()) 62 | { 63 | WriteEvent(MessageEventId, message); 64 | } 65 | } 66 | 67 | [NonEvent] 68 | public void ServiceMessage(ServiceContext serviceContext, string message, params object[] args) 69 | { 70 | if (this.IsEnabled()) 71 | { 72 | 73 | string finalMessage = string.Format(message, args); 74 | ServiceMessage( 75 | serviceContext.ServiceName.ToString(), 76 | serviceContext.ServiceTypeName, 77 | GetReplicaOrInstanceId(serviceContext), 78 | serviceContext.PartitionId, 79 | serviceContext.CodePackageActivationContext.ApplicationName, 80 | serviceContext.CodePackageActivationContext.ApplicationTypeName, 81 | serviceContext.NodeContext.NodeName, 82 | finalMessage); 83 | } 84 | } 85 | 86 | // For very high-frequency events it might be advantageous to raise events using WriteEventCore API. 87 | // This results in more efficient parameter handling, but requires explicit allocation of EventData structure and unsafe code. 88 | // To enable this code path, define UNSAFE conditional compilation symbol and turn on unsafe code support in project properties. 89 | private const int ServiceMessageEventId = 2; 90 | [Event(ServiceMessageEventId, Level = EventLevel.Informational, Message = "{7}")] 91 | private 92 | #if UNSAFE 93 | unsafe 94 | #endif 95 | void ServiceMessage( 96 | string serviceName, 97 | string serviceTypeName, 98 | long replicaOrInstanceId, 99 | Guid partitionId, 100 | string applicationName, 101 | string applicationTypeName, 102 | string nodeName, 103 | string message) 104 | { 105 | #if !UNSAFE 106 | WriteEvent(ServiceMessageEventId, serviceName, serviceTypeName, replicaOrInstanceId, partitionId, applicationName, applicationTypeName, nodeName, message); 107 | #else 108 | const int numArgs = 8; 109 | fixed (char* pServiceName = serviceName, pServiceTypeName = serviceTypeName, pApplicationName = applicationName, pApplicationTypeName = applicationTypeName, pNodeName = nodeName, pMessage = message) 110 | { 111 | EventData* eventData = stackalloc EventData[numArgs]; 112 | eventData[0] = new EventData { DataPointer = (IntPtr) pServiceName, Size = SizeInBytes(serviceName) }; 113 | eventData[1] = new EventData { DataPointer = (IntPtr) pServiceTypeName, Size = SizeInBytes(serviceTypeName) }; 114 | eventData[2] = new EventData { DataPointer = (IntPtr) (&replicaOrInstanceId), Size = sizeof(long) }; 115 | eventData[3] = new EventData { DataPointer = (IntPtr) (&partitionId), Size = sizeof(Guid) }; 116 | eventData[4] = new EventData { DataPointer = (IntPtr) pApplicationName, Size = SizeInBytes(applicationName) }; 117 | eventData[5] = new EventData { DataPointer = (IntPtr) pApplicationTypeName, Size = SizeInBytes(applicationTypeName) }; 118 | eventData[6] = new EventData { DataPointer = (IntPtr) pNodeName, Size = SizeInBytes(nodeName) }; 119 | eventData[7] = new EventData { DataPointer = (IntPtr) pMessage, Size = SizeInBytes(message) }; 120 | 121 | WriteEventCore(ServiceMessageEventId, numArgs, eventData); 122 | } 123 | #endif 124 | } 125 | 126 | private const int ServiceTypeRegisteredEventId = 3; 127 | [Event(ServiceTypeRegisteredEventId, Level = EventLevel.Informational, Message = "Service host process {0} registered service type {1}", Keywords = Keywords.ServiceInitialization)] 128 | public void ServiceTypeRegistered(int hostProcessId, string serviceType) 129 | { 130 | WriteEvent(ServiceTypeRegisteredEventId, hostProcessId, serviceType); 131 | } 132 | 133 | private const int ServiceHostInitializationFailedEventId = 4; 134 | [Event(ServiceHostInitializationFailedEventId, Level = EventLevel.Error, Message = "Service host initialization failed", Keywords = Keywords.ServiceInitialization)] 135 | public void ServiceHostInitializationFailed(string exception) 136 | { 137 | WriteEvent(ServiceHostInitializationFailedEventId, exception); 138 | } 139 | 140 | // A pair of events sharing the same name prefix with a "Start"/"Stop" suffix implicitly marks boundaries of an event tracing activity. 141 | // These activities can be automatically picked up by debugging and profiling tools, which can compute their execution time, child activities, 142 | // and other statistics. 143 | private const int ServiceRequestStartEventId = 5; 144 | [Event(ServiceRequestStartEventId, Level = EventLevel.Informational, Message = "Service request '{0}' started", Keywords = Keywords.Requests)] 145 | public void ServiceRequestStart(string requestTypeName) 146 | { 147 | WriteEvent(ServiceRequestStartEventId, requestTypeName); 148 | } 149 | 150 | private const int ServiceRequestStopEventId = 6; 151 | [Event(ServiceRequestStopEventId, Level = EventLevel.Informational, Message = "Service request '{0}' finished", Keywords = Keywords.Requests)] 152 | public void ServiceRequestStop(string requestTypeName, string exception = "") 153 | { 154 | WriteEvent(ServiceRequestStopEventId, requestTypeName, exception); 155 | } 156 | #endregion 157 | 158 | #region Private methods 159 | private static long GetReplicaOrInstanceId(ServiceContext context) 160 | { 161 | StatelessServiceContext stateless = context as StatelessServiceContext; 162 | if (stateless != null) 163 | { 164 | return stateless.InstanceId; 165 | } 166 | 167 | StatefulServiceContext stateful = context as StatefulServiceContext; 168 | if (stateful != null) 169 | { 170 | return stateful.ReplicaId; 171 | } 172 | 173 | throw new NotSupportedException("Context type not supported."); 174 | } 175 | #if UNSAFE 176 | private int SizeInBytes(string s) 177 | { 178 | if (s == null) 179 | { 180 | return 0; 181 | } 182 | else 183 | { 184 | return (s.Length + 1) * sizeof(char); 185 | } 186 | } 187 | #endif 188 | #endregion 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /examples/Services/ClientApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using SoCreate.Extensions.Caching.ServiceFabric; 8 | using System; 9 | 10 | namespace ClientApp 11 | { 12 | public class Startup 13 | { 14 | public Startup(IConfiguration configuration) 15 | { 16 | Configuration = configuration; 17 | } 18 | 19 | public IConfiguration Configuration { get; } 20 | 21 | // This method gets called by the runtime. Use this method to add services to the container. 22 | public void ConfigureServices(IServiceCollection services) 23 | { 24 | services.Configure(o => o.RetryTimeout = TimeSpan.FromSeconds(5)); 25 | services.AddDistributedServiceFabricCache(); 26 | 27 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0); 28 | } 29 | 30 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 31 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 32 | { 33 | if (env.IsDevelopment()) 34 | { 35 | app.UseDeveloperExceptionPage(); 36 | } 37 | 38 | app.UseRouting(); 39 | 40 | app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/Services/ClientApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/Services/ClientApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /examples/Services/DistributedCacheStore/DistributedCacheStore.cs: -------------------------------------------------------------------------------- 1 | using System.Fabric; 2 | using SoCreate.Extensions.Caching.ServiceFabric; 3 | 4 | namespace DistributedCacheStore 5 | { 6 | internal sealed partial class DistributedCacheStore : DistributedCacheStoreService 7 | { 8 | public DistributedCacheStore(StatefulServiceContext context) 9 | : base(context, (message) => ServiceEventSource.Current.ServiceMessage(context, message)) 10 | { } 11 | 12 | protected override int MaxCacheSizeInMegabytes => 1000; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/Services/DistributedCacheStore/DistributedCacheStore.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | True 7 | True 8 | win7-x64 9 | False 10 | 7.3 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/Services/DistributedCacheStore/PackageRoot/Config/Settings.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 |
5 | 6 |
7 | 8 |
9 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /examples/Services/DistributedCacheStore/PackageRoot/ServiceManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | DistributedCacheStore.exe 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /examples/Services/DistributedCacheStore/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.ServiceFabric.Services.Runtime; 6 | 7 | namespace DistributedCacheStore 8 | { 9 | internal static class Program 10 | { 11 | /// 12 | /// This is the entry point of the service host process. 13 | /// 14 | private static void Main() 15 | { 16 | try 17 | { 18 | // The ServiceManifest.XML file defines one or more service type names. 19 | // Registering a service maps a service type name to a .NET type. 20 | // When Service Fabric creates an instance of this service type, 21 | // an instance of the class is created in this host process. 22 | 23 | ServiceRuntime.RegisterServiceAsync("DistributedCacheStoreType", 24 | context => new DistributedCacheStore(context)).GetAwaiter().GetResult(); 25 | 26 | ServiceEventSource.Current.ServiceTypeRegistered(Process.GetCurrentProcess().Id, typeof(DistributedCacheStore).Name); 27 | 28 | // Prevents this host process from terminating so services keep running. 29 | Thread.Sleep(Timeout.Infinite); 30 | } 31 | catch (Exception e) 32 | { 33 | ServiceEventSource.Current.ServiceHostInitializationFailed(e.ToString()); 34 | throw; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/Services/DistributedCacheStore/ServiceEventSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.Tracing; 4 | using System.Fabric; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Microsoft.ServiceFabric.Services.Runtime; 9 | 10 | namespace DistributedCacheStore 11 | { 12 | [EventSource(Name = "MyCompany-DistributedCache-DistributedCacheStore")] 13 | internal sealed class ServiceEventSource : EventSource 14 | { 15 | public static readonly ServiceEventSource Current = new ServiceEventSource(); 16 | 17 | // Instance constructor is private to enforce singleton semantics 18 | private ServiceEventSource() : base() { } 19 | 20 | #region Keywords 21 | // Event keywords can be used to categorize events. 22 | // Each keyword is a bit flag. A single event can be associated with multiple keywords (via EventAttribute.Keywords property). 23 | // Keywords must be defined as a public class named 'Keywords' inside EventSource that uses them. 24 | public static class Keywords 25 | { 26 | public const EventKeywords Requests = (EventKeywords)0x1L; 27 | public const EventKeywords ServiceInitialization = (EventKeywords)0x2L; 28 | } 29 | #endregion 30 | 31 | #region Events 32 | // Define an instance method for each event you want to record and apply an [Event] attribute to it. 33 | // The method name is the name of the event. 34 | // Pass any parameters you want to record with the event (only primitive integer types, DateTime, Guid & string are allowed). 35 | // Each event method implementation should check whether the event source is enabled, and if it is, call WriteEvent() method to raise the event. 36 | // The number and types of arguments passed to every event method must exactly match what is passed to WriteEvent(). 37 | // Put [NonEvent] attribute on all methods that do not define an event. 38 | // For more information see https://msdn.microsoft.com/en-us/library/system.diagnostics.tracing.eventsource.aspx 39 | 40 | [NonEvent] 41 | public void Message(string message, params object[] args) 42 | { 43 | if (this.IsEnabled()) 44 | { 45 | string finalMessage = string.Format(message, args); 46 | Message(finalMessage); 47 | } 48 | } 49 | 50 | private const int MessageEventId = 1; 51 | [Event(MessageEventId, Level = EventLevel.Informational, Message = "{0}")] 52 | public void Message(string message) 53 | { 54 | if (this.IsEnabled()) 55 | { 56 | WriteEvent(MessageEventId, message); 57 | } 58 | } 59 | 60 | [NonEvent] 61 | public void ServiceMessage(StatefulServiceContext serviceContext, string message, params object[] args) 62 | { 63 | if (this.IsEnabled()) 64 | { 65 | string finalMessage = string.Format(message, args); 66 | ServiceMessage( 67 | serviceContext.ServiceName.ToString(), 68 | serviceContext.ServiceTypeName, 69 | serviceContext.ReplicaId, 70 | serviceContext.PartitionId, 71 | serviceContext.CodePackageActivationContext.ApplicationName, 72 | serviceContext.CodePackageActivationContext.ApplicationTypeName, 73 | serviceContext.NodeContext.NodeName, 74 | finalMessage); 75 | } 76 | } 77 | 78 | // For very high-frequency events it might be advantageous to raise events using WriteEventCore API. 79 | // This results in more efficient parameter handling, but requires explicit allocation of EventData structure and unsafe code. 80 | // To enable this code path, define UNSAFE conditional compilation symbol and turn on unsafe code support in project properties. 81 | private const int ServiceMessageEventId = 2; 82 | [Event(ServiceMessageEventId, Level = EventLevel.Informational, Message = "{7}")] 83 | private 84 | #if UNSAFE 85 | unsafe 86 | #endif 87 | void ServiceMessage( 88 | string serviceName, 89 | string serviceTypeName, 90 | long replicaOrInstanceId, 91 | Guid partitionId, 92 | string applicationName, 93 | string applicationTypeName, 94 | string nodeName, 95 | string message) 96 | { 97 | #if !UNSAFE 98 | WriteEvent(ServiceMessageEventId, serviceName, serviceTypeName, replicaOrInstanceId, partitionId, applicationName, applicationTypeName, nodeName, message); 99 | #else 100 | const int numArgs = 8; 101 | fixed (char* pServiceName = serviceName, pServiceTypeName = serviceTypeName, pApplicationName = applicationName, pApplicationTypeName = applicationTypeName, pNodeName = nodeName, pMessage = message) 102 | { 103 | EventData* eventData = stackalloc EventData[numArgs]; 104 | eventData[0] = new EventData { DataPointer = (IntPtr) pServiceName, Size = SizeInBytes(serviceName) }; 105 | eventData[1] = new EventData { DataPointer = (IntPtr) pServiceTypeName, Size = SizeInBytes(serviceTypeName) }; 106 | eventData[2] = new EventData { DataPointer = (IntPtr) (&replicaOrInstanceId), Size = sizeof(long) }; 107 | eventData[3] = new EventData { DataPointer = (IntPtr) (&partitionId), Size = sizeof(Guid) }; 108 | eventData[4] = new EventData { DataPointer = (IntPtr) pApplicationName, Size = SizeInBytes(applicationName) }; 109 | eventData[5] = new EventData { DataPointer = (IntPtr) pApplicationTypeName, Size = SizeInBytes(applicationTypeName) }; 110 | eventData[6] = new EventData { DataPointer = (IntPtr) pNodeName, Size = SizeInBytes(nodeName) }; 111 | eventData[7] = new EventData { DataPointer = (IntPtr) pMessage, Size = SizeInBytes(message) }; 112 | 113 | WriteEventCore(ServiceMessageEventId, numArgs, eventData); 114 | } 115 | #endif 116 | } 117 | 118 | private const int ServiceTypeRegisteredEventId = 3; 119 | [Event(ServiceTypeRegisteredEventId, Level = EventLevel.Informational, Message = "Service host process {0} registered service type {1}", Keywords = Keywords.ServiceInitialization)] 120 | public void ServiceTypeRegistered(int hostProcessId, string serviceType) 121 | { 122 | WriteEvent(ServiceTypeRegisteredEventId, hostProcessId, serviceType); 123 | } 124 | 125 | private const int ServiceHostInitializationFailedEventId = 4; 126 | [Event(ServiceHostInitializationFailedEventId, Level = EventLevel.Error, Message = "Service host initialization failed", Keywords = Keywords.ServiceInitialization)] 127 | public void ServiceHostInitializationFailed(string exception) 128 | { 129 | WriteEvent(ServiceHostInitializationFailedEventId, exception); 130 | } 131 | 132 | // A pair of events sharing the same name prefix with a "Start"/"Stop" suffix implicitly marks boundaries of an event tracing activity. 133 | // These activities can be automatically picked up by debugging and profiling tools, which can compute their execution time, child activities, 134 | // and other statistics. 135 | private const int ServiceRequestStartEventId = 5; 136 | [Event(ServiceRequestStartEventId, Level = EventLevel.Informational, Message = "Service request '{0}' started", Keywords = Keywords.Requests)] 137 | public void ServiceRequestStart(string requestTypeName) 138 | { 139 | WriteEvent(ServiceRequestStartEventId, requestTypeName); 140 | } 141 | 142 | private const int ServiceRequestStopEventId = 6; 143 | [Event(ServiceRequestStopEventId, Level = EventLevel.Informational, Message = "Service request '{0}' finished", Keywords = Keywords.Requests)] 144 | public void ServiceRequestStop(string requestTypeName, string exception = "") 145 | { 146 | WriteEvent(ServiceRequestStopEventId, requestTypeName, exception); 147 | } 148 | #endregion 149 | 150 | #region Private methods 151 | #if UNSAFE 152 | private int SizeInBytes(string s) 153 | { 154 | if (s == null) 155 | { 156 | return 0; 157 | } 158 | else 159 | { 160 | return (s.Length + 1) * sizeof(char); 161 | } 162 | } 163 | #endif 164 | #endregion 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/CacheStoreException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SoCreate.Extensions.Caching.ServiceFabric 4 | { 5 | public class CacheStoreException : Exception 6 | { 7 | internal CacheStoreException(string message) : base(message) 8 | { 9 | 10 | } 11 | 12 | internal CacheStoreException(string message, Exception innerException) : base(message, innerException) 13 | { 14 | 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/CacheStoreMetadata.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ServiceFabric.Data; 2 | using System.IO; 3 | 4 | namespace SoCreate.Extensions.Caching.ServiceFabric 5 | { 6 | public sealed class CacheStoreMetadata 7 | { 8 | public CacheStoreMetadata(int size, string firstCacheKey, string lastCacheKey) 9 | { 10 | Size = size; 11 | FirstCacheKey = firstCacheKey; 12 | LastCacheKey = lastCacheKey; 13 | } 14 | 15 | public int Size { get; private set; } 16 | public string FirstCacheKey { get; private set; } 17 | public string LastCacheKey { get; private set; } 18 | } 19 | 20 | class CacheStoreMetadataSerializer : IStateSerializer 21 | { 22 | CacheStoreMetadata IStateSerializer.Read(BinaryReader reader) 23 | { 24 | return new CacheStoreMetadata( 25 | reader.ReadInt32(), 26 | GetStringValueOrNull(reader.ReadString()), 27 | GetStringValueOrNull(reader.ReadString()) 28 | ); 29 | } 30 | 31 | void IStateSerializer.Write(CacheStoreMetadata value, BinaryWriter writer) 32 | { 33 | writer.Write(value.Size); 34 | writer.Write(value.FirstCacheKey ?? string.Empty); 35 | writer.Write(value.LastCacheKey ?? string.Empty); 36 | } 37 | 38 | // Read overload for differential de-serialization 39 | CacheStoreMetadata IStateSerializer.Read(CacheStoreMetadata baseValue, BinaryReader reader) 40 | { 41 | return ((IStateSerializer)this).Read(reader); 42 | } 43 | 44 | // Write overload for differential serialization 45 | void IStateSerializer.Write(CacheStoreMetadata baseValue, CacheStoreMetadata newValue, BinaryWriter writer) 46 | { 47 | ((IStateSerializer)this).Write(newValue, writer); 48 | } 49 | 50 | private string GetStringValueOrNull(string value) 51 | { 52 | return value == string.Empty ? null : value; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/CacheStoreNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace SoCreate.Extensions.Caching.ServiceFabric 2 | { 3 | class CacheStoreNotFoundException : CacheStoreException 4 | { 5 | internal CacheStoreNotFoundException(string message) : base(message) 6 | { 7 | 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/CachedItem.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ServiceFabric.Data; 2 | using System; 3 | using System.IO; 4 | 5 | namespace SoCreate.Extensions.Caching.ServiceFabric 6 | { 7 | public sealed class CachedItem 8 | { 9 | public CachedItem(byte[] value, string beforeCacheKey = null, string afterCacheKey = null, TimeSpan? slidingExpiration = null, DateTimeOffset? absoluteExpiration = null) 10 | { 11 | Value = value; 12 | BeforeCacheKey = beforeCacheKey; 13 | AfterCacheKey = afterCacheKey; 14 | SlidingExpiration = slidingExpiration; 15 | AbsoluteExpiration = absoluteExpiration; 16 | } 17 | 18 | public byte[] Value { get; private set; } 19 | public string BeforeCacheKey { get; private set; } 20 | public string AfterCacheKey { get; private set; } 21 | public TimeSpan? SlidingExpiration { get; private set; } 22 | public DateTimeOffset? AbsoluteExpiration { get; private set; } 23 | } 24 | 25 | class CachedItemSerializer : IStateSerializer 26 | { 27 | CachedItem IStateSerializer.Read(BinaryReader reader) 28 | { 29 | var byteLength = reader.ReadInt32(); 30 | return new CachedItem( 31 | reader.ReadBytes(byteLength), 32 | GetStringValueOrNull(reader.ReadString()), 33 | GetStringValueOrNull(reader.ReadString()), 34 | GetTimeSpanFromTicks(reader.ReadInt64()), 35 | GetDateTimeOffsetFromDateData(reader.ReadInt64(), reader.ReadInt64()) 36 | ); 37 | } 38 | 39 | void IStateSerializer.Write(CachedItem value, BinaryWriter writer) 40 | { 41 | writer.Write(value.Value.Length); 42 | writer.Write(value.Value); 43 | writer.Write(value.BeforeCacheKey ?? string.Empty); 44 | writer.Write(value.AfterCacheKey ?? string.Empty); 45 | writer.Write(GetTicksFromTimeSpan(value.SlidingExpiration)); 46 | writer.Write(GetLongDateTimeFromDateTimeOffset(value.AbsoluteExpiration)); 47 | writer.Write(GetShortOffsetFromDateTimeOffset(value.AbsoluteExpiration)); 48 | } 49 | 50 | // Read overload for differential de-serialization 51 | CachedItem IStateSerializer.Read(CachedItem baseValue, BinaryReader reader) 52 | { 53 | return ((IStateSerializer)this).Read(reader); 54 | } 55 | 56 | // Write overload for differential serialization 57 | void IStateSerializer.Write(CachedItem baseValue, CachedItem newValue, BinaryWriter writer) 58 | { 59 | ((IStateSerializer)this).Write(newValue, writer); 60 | } 61 | 62 | private string GetStringValueOrNull(string value) 63 | { 64 | return value == string.Empty ? null : value; 65 | } 66 | 67 | private TimeSpan? GetTimeSpanFromTicks(long ticks) 68 | { 69 | if (ticks == 0) return null; 70 | 71 | return TimeSpan.FromTicks(ticks); 72 | } 73 | 74 | private long GetTicksFromTimeSpan(TimeSpan? timeSpan) 75 | { 76 | if (!timeSpan.HasValue) return 0; 77 | 78 | return timeSpan.Value.Ticks; 79 | } 80 | 81 | private DateTimeOffset? GetDateTimeOffsetFromDateData(long dateDataTicks, long offsetTicks) 82 | { 83 | return new DateTimeOffset(DateTime.FromBinary(dateDataTicks), new TimeSpan(offsetTicks)); 84 | } 85 | 86 | private long GetLongDateTimeFromDateTimeOffset(DateTimeOffset? dateTimeOffset) 87 | { 88 | if (!dateTimeOffset.HasValue) return 0; 89 | return dateTimeOffset.Value.Ticks; 90 | } 91 | 92 | private long GetShortOffsetFromDateTimeOffset(DateTimeOffset? dateTimeOffset) 93 | { 94 | if (!dateTimeOffset.HasValue) return 0; 95 | return dateTimeOffset.Value.Offset.Ticks; 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/DistributedCacheStoreLocator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using Microsoft.ServiceFabric.Services.Client; 3 | using Microsoft.ServiceFabric.Services.Communication.Client; 4 | using Microsoft.ServiceFabric.Services.Remoting.Client; 5 | using Microsoft.ServiceFabric.Services.Remoting.V2; 6 | using Microsoft.ServiceFabric.Services.Remoting.V2.FabricTransport.Client; 7 | using Microsoft.VisualStudio.Threading; 8 | using System; 9 | using System.Collections.Concurrent; 10 | using System.Fabric; 11 | using System.Fabric.Description; 12 | using System.Fabric.Query; 13 | using System.Linq; 14 | using System.Security.Cryptography; 15 | using System.Text; 16 | using System.Threading.Tasks; 17 | 18 | namespace SoCreate.Extensions.Caching.ServiceFabric 19 | { 20 | class DistributedCacheStoreLocator : IDistributedCacheStoreLocator 21 | { 22 | private const string CacheStoreProperty = "CacheStore"; 23 | private const string CacheStorePropertyValue = "true"; 24 | private const string ListenerName = "CacheStoreServiceListener"; 25 | private AsyncLazy _serviceUri; 26 | private readonly string _endpointName; 27 | private readonly TimeSpan? _retryTimeout; 28 | private readonly FabricClient _fabricClient; 29 | private AsyncLazy _partitionList; 30 | private readonly ConcurrentDictionary _cacheStores; 31 | private readonly ServiceFabricCacheOptions _options; 32 | private readonly IServiceRemotingMessageSerializationProvider _serializationProvider; 33 | 34 | public DistributedCacheStoreLocator(IOptions options) 35 | { 36 | _options = options.Value; 37 | _endpointName = _options.CacheStoreEndpointName ?? ListenerName; 38 | _retryTimeout = _options.RetryTimeout; 39 | _serializationProvider = _options.SerializationProvider; 40 | _fabricClient = new FabricClient(); 41 | _cacheStores = new ConcurrentDictionary(); 42 | _serviceUri = new AsyncLazy(LocateCacheStoreAsync); 43 | _partitionList = new AsyncLazy(GetPartitionListAsync); 44 | } 45 | 46 | public async Task GetCacheStoreProxy(string cacheKey) 47 | { 48 | var partitionInformation = await GetPartitionInformationForCacheKeyAsync(cacheKey); 49 | 50 | var serviceUri = await _serviceUri.GetValueAsync(); 51 | 52 | return _cacheStores.GetOrAdd(partitionInformation.Id, key => 53 | { 54 | var info = (Int64RangePartitionInformation)partitionInformation; 55 | var resolvedPartition = new ServicePartitionKey(info.LowKey); 56 | var retrySettings = _retryTimeout.HasValue ? new OperationRetrySettings(_retryTimeout.Value) : null; 57 | 58 | var proxyFactory = new ServiceProxyFactory((c) => 59 | { 60 | return new FabricTransportServiceRemotingClientFactory(serializationProvider: _serializationProvider); 61 | }, retrySettings); 62 | 63 | return proxyFactory.CreateServiceProxy(serviceUri, resolvedPartition, TargetReplicaSelector.Default, _endpointName); 64 | }); 65 | } 66 | 67 | private async Task GetPartitionInformationForCacheKeyAsync(string cacheKey) 68 | { 69 | var md5 = MD5.Create(); 70 | var value = md5.ComputeHash(Encoding.ASCII.GetBytes(cacheKey)); 71 | var key = BitConverter.ToInt64(value, 0); 72 | 73 | var partition = (await _partitionList.GetValueAsync()).Single(p => ((Int64RangePartitionInformation)p.PartitionInformation).LowKey <= key && ((Int64RangePartitionInformation)p.PartitionInformation).HighKey >= key); 74 | return partition.PartitionInformation; 75 | } 76 | 77 | private async Task GetPartitionListAsync() 78 | { 79 | return await _fabricClient.QueryManager.GetPartitionListAsync(await _serviceUri.GetValueAsync()); 80 | } 81 | 82 | 83 | private async Task LocateCacheStoreAsync() 84 | { 85 | if (_options.CacheStoreServiceUri != null) 86 | { 87 | return _options.CacheStoreServiceUri; 88 | } 89 | try 90 | { 91 | bool hasPages = true; 92 | var query = new ApplicationQueryDescription() { MaxResults = 50 }; 93 | 94 | while (hasPages) 95 | { 96 | var apps = await _fabricClient.QueryManager.GetApplicationPagedListAsync(query); 97 | 98 | query.ContinuationToken = apps.ContinuationToken; 99 | 100 | hasPages = !string.IsNullOrEmpty(query.ContinuationToken); 101 | 102 | foreach (var app in apps) 103 | { 104 | var serviceName = await LocateCacheStoreServiceInApplicationAsync(app.ApplicationName); 105 | if (serviceName != null) 106 | return serviceName; 107 | } 108 | } 109 | } 110 | catch { } 111 | 112 | throw new CacheStoreNotFoundException("Cache store not found in Service Fabric cluster. Try setting the 'CacheStoreServiceUri' configuration option to the location of your cache store."); ; 113 | } 114 | 115 | private async Task LocateCacheStoreServiceInApplicationAsync(Uri applicationName) 116 | { 117 | try 118 | { 119 | bool hasPages = true; 120 | var query = new ServiceQueryDescription(applicationName) { MaxResults = 50 }; 121 | 122 | while (hasPages) 123 | { 124 | var services = await _fabricClient.QueryManager.GetServicePagedListAsync(query); 125 | 126 | query.ContinuationToken = services.ContinuationToken; 127 | 128 | hasPages = !string.IsNullOrEmpty(query.ContinuationToken); 129 | 130 | foreach (var service in services) 131 | { 132 | var found = await IsCacheStore(service.ServiceName); 133 | if (found) 134 | return service.ServiceName; 135 | } 136 | } 137 | } 138 | catch { } 139 | 140 | return null; 141 | } 142 | 143 | private async Task IsCacheStore(Uri serviceName) 144 | { 145 | try 146 | { 147 | var isCacheStore = await _fabricClient.PropertyManager.GetPropertyAsync(serviceName, CacheStoreProperty); 148 | return isCacheStore.GetValue() == CacheStorePropertyValue; 149 | } 150 | catch { } 151 | 152 | return false; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/DistributedCacheStoreService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Internal; 2 | using Microsoft.ServiceFabric.Data; 3 | using Microsoft.ServiceFabric.Data.Collections; 4 | using Microsoft.ServiceFabric.Services.Communication.Runtime; 5 | using Microsoft.ServiceFabric.Services.Remoting.V2; 6 | using Microsoft.ServiceFabric.Services.Remoting.V2.FabricTransport.Runtime; 7 | using Microsoft.ServiceFabric.Services.Runtime; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Fabric; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | namespace SoCreate.Extensions.Caching.ServiceFabric 15 | { 16 | public abstract class DistributedCacheStoreService : StatefulService, IServiceFabricCacheStoreService 17 | { 18 | private const string CacheStoreProperty = "CacheStore"; 19 | private const string CacheStorePropertyValue = "true"; 20 | const int BytesInMegabyte = 1048576; 21 | const int ByteSizeOffset = 250; 22 | const int DefaultCacheSizeInMegabytes = 100; 23 | const string CacheStoreName = "CacheStore"; 24 | const string CacheStoreMetadataName = "CacheStoreMetadata"; 25 | const string CacheStoreMetadataKey = "CacheStoreMetadata"; 26 | private const string ListenerName = "CacheStoreServiceListener"; 27 | private readonly Uri _serviceUri; 28 | private readonly IReliableStateManagerReplica2 _reliableStateManagerReplica; 29 | private readonly Action _log; 30 | private readonly ISystemClock _systemClock; 31 | private int _partitionCount = 1; 32 | protected IServiceRemotingMessageSerializationProvider _serializationProvider; 33 | 34 | public DistributedCacheStoreService(StatefulServiceContext context, Action log = null, IServiceRemotingMessageSerializationProvider serializationProvider = null) 35 | : base(context) 36 | { 37 | _serviceUri = context.ServiceName; 38 | _log = log; 39 | _systemClock = new SystemClock(); 40 | _serializationProvider = serializationProvider; 41 | 42 | if (!StateManager.TryAddStateSerializer(new CachedItemSerializer())) 43 | { 44 | throw new InvalidOperationException("Failed to set CachedItem custom serializer"); 45 | } 46 | 47 | if (!StateManager.TryAddStateSerializer(new CacheStoreMetadataSerializer())) 48 | { 49 | throw new InvalidOperationException("Failed to set CacheStoreMetadata custom serializer"); 50 | } 51 | } 52 | 53 | public DistributedCacheStoreService(StatefulServiceContext context, IReliableStateManagerReplica2 reliableStateManagerReplica, ISystemClock systemClock, Action log) 54 | : base(context, reliableStateManagerReplica) 55 | { 56 | _serviceUri = context.ServiceName; 57 | _reliableStateManagerReplica = reliableStateManagerReplica; 58 | _log = log; 59 | _systemClock = systemClock; 60 | } 61 | 62 | protected async override Task OnOpenAsync(ReplicaOpenMode openMode, CancellationToken cancellationToken) 63 | { 64 | var client = new FabricClient(); 65 | await client.PropertyManager.PutPropertyAsync(_serviceUri, CacheStoreProperty, CacheStorePropertyValue); 66 | _partitionCount = (await client.QueryManager.GetPartitionListAsync(_serviceUri)).Count; 67 | } 68 | 69 | protected virtual int MaxCacheSizeInMegabytes { get { return DefaultCacheSizeInMegabytes; } } 70 | 71 | public async Task GetCachedItemAsync(string key) 72 | { 73 | var cacheStore = await StateManager.GetOrAddAsync>(CacheStoreName); 74 | 75 | var cacheResult = await RetryHelper.ExecuteWithRetry(StateManager, async (tx, cancellationToken, state) => 76 | { 77 | _log?.Invoke($"Get cached item called with key: {key} on partition id: {Partition?.PartitionInfo.Id}"); 78 | return await cacheStore.TryGetValueAsync(tx, key); 79 | }); 80 | 81 | if (cacheResult.HasValue) 82 | { 83 | var cachedItem = cacheResult.Value; 84 | var expireTime = cachedItem.AbsoluteExpiration; 85 | 86 | // cache item not expired 87 | if (_systemClock.UtcNow < expireTime) 88 | { 89 | await SetCachedItemAsync(key, cachedItem.Value, cachedItem.SlidingExpiration, cachedItem.AbsoluteExpiration); 90 | return cachedItem.Value; 91 | } 92 | } 93 | 94 | return null; 95 | } 96 | 97 | public async Task SetCachedItemAsync(string key, byte[] value, TimeSpan? slidingExpiration, DateTimeOffset? absoluteExpiration) 98 | { 99 | if (slidingExpiration.HasValue) 100 | { 101 | var now = _systemClock.UtcNow; 102 | absoluteExpiration = now.AddMilliseconds(slidingExpiration.Value.TotalMilliseconds); 103 | } 104 | 105 | var cacheStore = await StateManager.GetOrAddAsync>(CacheStoreName); 106 | var cacheStoreMetadata = await StateManager.GetOrAddAsync>(CacheStoreMetadataName); 107 | 108 | await RetryHelper.ExecuteWithRetry(StateManager, async (tx, cancellationToken, state) => 109 | { 110 | _log?.Invoke($"Set cached item called with key: {key} on partition id: {Partition?.PartitionInfo.Id}"); 111 | 112 | Func>> getCacheItem = async (string cacheKey) => await cacheStore.TryGetValueAsync(tx, cacheKey, LockMode.Update); 113 | var linkedDictionaryHelper = new LinkedDictionaryHelper(getCacheItem, ByteSizeOffset); 114 | 115 | var cacheStoreInfo = (await cacheStoreMetadata.TryGetValueAsync(tx, CacheStoreMetadataKey, LockMode.Update)).Value ?? new CacheStoreMetadata(0, null, null); 116 | var existingCacheItem = (await getCacheItem(key)).Value; 117 | var cachedItem = ApplyAbsoluteExpiration(existingCacheItem, absoluteExpiration) ?? new CachedItem(value, null, null, slidingExpiration, absoluteExpiration); 118 | 119 | // empty linked dictionary 120 | if (cacheStoreInfo.FirstCacheKey == null) 121 | { 122 | var metadata = new CacheStoreMetadata(value.Length + ByteSizeOffset, key, key); 123 | await cacheStoreMetadata.SetAsync(tx, CacheStoreMetadataKey, metadata); 124 | await cacheStore.SetAsync(tx, key, cachedItem); 125 | } 126 | else 127 | { 128 | var cacheMetadata = cacheStoreInfo; 129 | 130 | // linked node already exists in dictionary 131 | if (existingCacheItem != null) 132 | { 133 | var removeResult = await linkedDictionaryHelper.Remove(cacheStoreInfo, cachedItem); 134 | cacheMetadata = removeResult.CacheStoreMetadata; 135 | await ApplyChanges(tx, cacheStore, cacheStoreMetadata, removeResult); 136 | } 137 | 138 | // add to last 139 | var addLastResult = await linkedDictionaryHelper.AddLast(cacheMetadata, key, cachedItem, value); 140 | await ApplyChanges(tx, cacheStore, cacheStoreMetadata, addLastResult); 141 | } 142 | }); 143 | } 144 | 145 | public async Task RemoveCachedItemAsync(string key) 146 | { 147 | var cacheStore = await StateManager.GetOrAddAsync>(CacheStoreName); 148 | var cacheStoreMetadata = await StateManager.GetOrAddAsync>(CacheStoreMetadataName); 149 | 150 | await RetryHelper.ExecuteWithRetry(StateManager, async (tx, cancellationToken, state) => 151 | { 152 | _log?.Invoke($"Remove cached item called with key: {key} on partition id: {Partition?.PartitionInfo.Id}"); 153 | 154 | var cacheResult = await cacheStore.TryRemoveAsync(tx, key); 155 | if (cacheResult.HasValue) 156 | { 157 | Func>> getCacheItem = async (string cacheKey) => await cacheStore.TryGetValueAsync(tx, cacheKey, LockMode.Update); 158 | var linkedDictionaryHelper = new LinkedDictionaryHelper(getCacheItem, ByteSizeOffset); 159 | 160 | var cacheStoreInfo = (await cacheStoreMetadata.TryGetValueAsync(tx, CacheStoreMetadataKey, LockMode.Update)).Value ?? new CacheStoreMetadata(0, null, null); 161 | var result = await linkedDictionaryHelper.Remove(cacheStoreInfo, cacheResult.Value); 162 | 163 | await ApplyChanges(tx, cacheStore, cacheStoreMetadata, result); 164 | } 165 | }); 166 | } 167 | 168 | protected override IEnumerable CreateServiceReplicaListeners() 169 | { 170 | yield return new ServiceReplicaListener(context => 171 | new FabricTransportServiceRemotingListener(context, this, serializationProvider: _serializationProvider), ListenerName); 172 | } 173 | 174 | protected override async Task RunAsync(CancellationToken cancellationToken) 175 | { 176 | var cacheStore = await StateManager.GetOrAddAsync>(CacheStoreName); 177 | var cacheStoreMetadata = await StateManager.GetOrAddAsync>(CacheStoreMetadataName); 178 | 179 | while (true) 180 | { 181 | await RemoveLeastRecentlyUsedCacheItemWhenOverMaxCacheSize(cancellationToken); 182 | await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken); 183 | } 184 | } 185 | 186 | protected async Task RemoveLeastRecentlyUsedCacheItemWhenOverMaxCacheSize(CancellationToken cancellationToken) 187 | { 188 | var cacheStore = await StateManager.GetOrAddAsync>(CacheStoreName); 189 | var cacheStoreMetadata = await StateManager.GetOrAddAsync>(CacheStoreMetadataName); 190 | bool continueRemovingItems = true; 191 | 192 | while (continueRemovingItems) 193 | { 194 | continueRemovingItems = false; 195 | cancellationToken.ThrowIfCancellationRequested(); 196 | 197 | await RetryHelper.ExecuteWithRetry(StateManager, async (tx, cancelToken, state) => 198 | { 199 | var metadata = await cacheStoreMetadata.TryGetValueAsync(tx, CacheStoreMetadataKey, LockMode.Update); 200 | 201 | if (metadata.HasValue && !string.IsNullOrEmpty(metadata.Value.FirstCacheKey)) 202 | { 203 | _log?.Invoke($"Size: {metadata.Value.Size} Max Size: {GetMaxSizeInBytes()}"); 204 | 205 | if (metadata.Value.Size > GetMaxSizeInBytes()) 206 | { 207 | Func>> getCacheItem = async (string cacheKey) => await cacheStore.TryGetValueAsync(tx, cacheKey, LockMode.Update); 208 | var linkedDictionaryHelper = new LinkedDictionaryHelper(getCacheItem, ByteSizeOffset); 209 | 210 | var firstItemKey = metadata.Value.FirstCacheKey; 211 | 212 | var firstCachedItem = (await getCacheItem(firstItemKey)).Value; 213 | 214 | if (firstCachedItem != null) 215 | { 216 | // Move item to last item if cached item is not expired 217 | if (firstCachedItem.AbsoluteExpiration > _systemClock.UtcNow) 218 | { 219 | // remove cached item 220 | var removeResult = await linkedDictionaryHelper.Remove(metadata.Value, firstCachedItem); 221 | await ApplyChanges(tx, cacheStore, cacheStoreMetadata, removeResult); 222 | 223 | // add to last 224 | var addLastResult = await linkedDictionaryHelper.AddLast(removeResult.CacheStoreMetadata, firstItemKey, firstCachedItem, firstCachedItem.Value); 225 | await ApplyChanges(tx, cacheStore, cacheStoreMetadata, addLastResult); 226 | 227 | continueRemovingItems = addLastResult.CacheStoreMetadata.Size > GetMaxSizeInBytes(); 228 | } 229 | else // Remove 230 | { 231 | _log?.Invoke($"Auto Removing: {metadata.Value.FirstCacheKey}"); 232 | 233 | var result = await linkedDictionaryHelper.Remove(metadata.Value, firstCachedItem); 234 | await ApplyChanges(tx, cacheStore, cacheStoreMetadata, result); 235 | await cacheStore.TryRemoveAsync(tx, metadata.Value.FirstCacheKey); 236 | 237 | continueRemovingItems = result.CacheStoreMetadata.Size > GetMaxSizeInBytes(); 238 | } 239 | } 240 | } 241 | } 242 | }); 243 | await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken); 244 | } 245 | } 246 | 247 | private int GetMaxSizeInBytes() 248 | { 249 | return (MaxCacheSizeInMegabytes * BytesInMegabyte) / _partitionCount; 250 | } 251 | 252 | private async Task ApplyChanges(ITransaction tx, IReliableDictionary cachedItemStore, IReliableDictionary cacheStoreMetadata, LinkedDictionaryItemsChanged linkedDictionaryItemsChanged) 253 | { 254 | foreach (var cacheItem in linkedDictionaryItemsChanged.CachedItemsToUpdate) 255 | { 256 | await cachedItemStore.SetAsync(tx, cacheItem.Key, cacheItem.Value); 257 | } 258 | 259 | await cacheStoreMetadata.SetAsync(tx, CacheStoreMetadataKey, linkedDictionaryItemsChanged.CacheStoreMetadata); 260 | } 261 | 262 | private CachedItem ApplyAbsoluteExpiration(CachedItem cachedItem, DateTimeOffset? absoluteExpiration) 263 | { 264 | if (cachedItem != null) 265 | { 266 | return new CachedItem(cachedItem.Value, cachedItem.BeforeCacheKey, cachedItem.AfterCacheKey, cachedItem.SlidingExpiration, absoluteExpiration); 267 | } 268 | return null; 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/IDistributedCacheStoreLocator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace SoCreate.Extensions.Caching.ServiceFabric 4 | { 5 | interface IDistributedCacheStoreLocator 6 | { 7 | Task GetCacheStoreProxy(string cacheKey); 8 | } 9 | } -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/IServiceFabricCacheStoreService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ServiceFabric.Services.Remoting; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace SoCreate.Extensions.Caching.ServiceFabric 6 | { 7 | public interface IServiceFabricCacheStoreService : IService 8 | { 9 | Task GetCachedItemAsync(string key); 10 | Task SetCachedItemAsync(string key, byte[] value, TimeSpan? slidingExpiration, DateTimeOffset? absoluteExpiration); 11 | Task RemoveCachedItemAsync(string key); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/LinkedDictionaryHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ServiceFabric.Data; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace SoCreate.Extensions.Caching.ServiceFabric 7 | { 8 | class LinkedDictionaryHelper 9 | { 10 | private readonly Func>> _getCacheItem; 11 | private readonly int _byteSizeOffset; 12 | 13 | public LinkedDictionaryHelper(Func>> getCacheItem) : this(getCacheItem, 0) 14 | { 15 | } 16 | 17 | public LinkedDictionaryHelper(Func>> getCacheItem, int byteSizeOffset) 18 | { 19 | _getCacheItem = getCacheItem; 20 | _byteSizeOffset = byteSizeOffset; 21 | } 22 | 23 | public async Task Remove(CacheStoreMetadata cacheStoreMetadata, CachedItem cachedItem) 24 | { 25 | var before = cachedItem.BeforeCacheKey; 26 | var after = cachedItem.AfterCacheKey; 27 | var size = (cacheStoreMetadata.Size - cachedItem.Value.Length) - _byteSizeOffset; 28 | 29 | // only item in linked dictionary 30 | if (before == null && after == null) 31 | { 32 | return new LinkedDictionaryItemsChanged(new Dictionary(), new CacheStoreMetadata(size, null, null)); 33 | } 34 | 35 | // first item in linked dictionary 36 | if (before == null) 37 | { 38 | var afterCachedItem = (await _getCacheItem(after)).Value; 39 | var newCachedItem = new Dictionary { { after, new CachedItem(afterCachedItem.Value, null, afterCachedItem.AfterCacheKey, afterCachedItem.SlidingExpiration, afterCachedItem.AbsoluteExpiration) } }; 40 | return new LinkedDictionaryItemsChanged(newCachedItem, new CacheStoreMetadata(size, after, cacheStoreMetadata.LastCacheKey)); 41 | } 42 | 43 | // last item in linked dictionary 44 | if (after == null) 45 | { 46 | var beforeCachedItem = (await _getCacheItem(before)).Value; 47 | var newCachedItem = new Dictionary { { before, new CachedItem(beforeCachedItem.Value, beforeCachedItem.BeforeCacheKey, null, beforeCachedItem.SlidingExpiration, beforeCachedItem.AbsoluteExpiration) } }; 48 | return new LinkedDictionaryItemsChanged(newCachedItem, new CacheStoreMetadata(size, cacheStoreMetadata.FirstCacheKey, before)); 49 | } 50 | 51 | // middle item in linked dictionary 52 | 53 | var beforeItem = (await _getCacheItem(before)).Value; 54 | var afterItem = (await _getCacheItem(after)).Value; 55 | 56 | var metadata = new CacheStoreMetadata(size, cacheStoreMetadata.FirstCacheKey, cacheStoreMetadata.LastCacheKey); 57 | 58 | var newCachedItems = new Dictionary(); 59 | // add new before cached item 60 | newCachedItems.Add(before, new CachedItem(beforeItem.Value, beforeItem.BeforeCacheKey, after, beforeItem.SlidingExpiration, beforeItem.AbsoluteExpiration)); 61 | // add new after cached item 62 | newCachedItems.Add(after, new CachedItem(afterItem.Value, before, afterItem.AfterCacheKey, afterItem.SlidingExpiration, afterItem.AbsoluteExpiration)); 63 | 64 | return new LinkedDictionaryItemsChanged(newCachedItems, metadata); 65 | } 66 | 67 | public async Task AddLast(CacheStoreMetadata cacheStoreMetadata, string cacheItemKey, CachedItem cachedItem, byte[] newValue) 68 | { 69 | var cachedDictionary = new Dictionary(); 70 | var firstCacheKey = cacheItemKey; 71 | 72 | // set current last item to be the second from last 73 | if (cacheStoreMetadata.LastCacheKey != null) 74 | { 75 | var currentLastCacheItem = (await _getCacheItem(cacheStoreMetadata.LastCacheKey)).Value; 76 | firstCacheKey = cacheStoreMetadata.FirstCacheKey; 77 | cachedDictionary.Add(cacheStoreMetadata.LastCacheKey, new CachedItem(currentLastCacheItem.Value, currentLastCacheItem.BeforeCacheKey, cacheItemKey, currentLastCacheItem.SlidingExpiration, currentLastCacheItem.AbsoluteExpiration)); 78 | } 79 | 80 | // set new cached item to be last item in list 81 | cachedDictionary.Add(cacheItemKey, new CachedItem(newValue, cacheStoreMetadata.LastCacheKey, null, cachedItem.SlidingExpiration, cachedItem.AbsoluteExpiration)); 82 | 83 | // calculate size of new collection 84 | var size = (cacheStoreMetadata.Size + newValue.Length) + _byteSizeOffset; 85 | 86 | // set new last item in the metadata 87 | var newCacheStoreMetadata = new CacheStoreMetadata(size, firstCacheKey, cacheItemKey); 88 | 89 | return new LinkedDictionaryItemsChanged(cachedDictionary, newCacheStoreMetadata); 90 | } 91 | } 92 | 93 | class LinkedDictionaryItemsChanged 94 | { 95 | public LinkedDictionaryItemsChanged(Dictionary cachedItemsToUpdate, CacheStoreMetadata cacheStoreMetadata) 96 | { 97 | CachedItemsToUpdate = cachedItemsToUpdate; 98 | CacheStoreMetadata = cacheStoreMetadata; 99 | } 100 | 101 | public IReadOnlyDictionary CachedItemsToUpdate { get; private set; } 102 | public CacheStoreMetadata CacheStoreMetadata { get; private set; } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/RetryHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.ServiceFabric.Data; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace SoCreate.Extensions.Caching.ServiceFabric 7 | { 8 | static class RetryHelper 9 | { 10 | private const int DefaultMaxAttempts = 10; 11 | private static readonly TimeSpan InitialDelay = TimeSpan.FromMilliseconds(200); 12 | private static readonly TimeSpan MinimumDelay = TimeSpan.FromMilliseconds(200); 13 | 14 | public static async Task ExecuteWithRetry( 15 | IReliableStateManager stateManager, 16 | Func> operation, 17 | object state = null, 18 | CancellationToken cancellationToken = default(CancellationToken), 19 | int maxAttempts = DefaultMaxAttempts, 20 | TimeSpan? initialDelay = null) 21 | { 22 | if (stateManager == null) throw new ArgumentNullException(nameof(stateManager)); 23 | if (operation == null) throw new ArgumentNullException(nameof(operation)); 24 | if (maxAttempts <= 0) maxAttempts = DefaultMaxAttempts; 25 | if (initialDelay == null || initialDelay.Value < MinimumDelay) 26 | initialDelay = InitialDelay; 27 | 28 | Func> wrapped = async (token, st) => 29 | { 30 | TResult result; 31 | using (var tran = stateManager.CreateTransaction()) 32 | { 33 | try 34 | { 35 | result = await operation(tran, cancellationToken, state); 36 | await tran.CommitAsync(); 37 | } 38 | catch (TimeoutException) 39 | { 40 | tran.Abort(); 41 | throw; 42 | } 43 | } 44 | return result; 45 | }; 46 | 47 | var outerResult = await ExecuteWithRetry(wrapped, state, cancellationToken, maxAttempts, initialDelay); 48 | return outerResult; 49 | } 50 | 51 | public static async Task ExecuteWithRetry( 52 | IReliableStateManager stateManager, 53 | Func operation, 54 | object state = null, 55 | CancellationToken cancellationToken = default(CancellationToken), 56 | int maxAttempts = DefaultMaxAttempts, 57 | TimeSpan? initialDelay = null) 58 | { 59 | if (stateManager == null) throw new ArgumentNullException(nameof(stateManager)); 60 | if (operation == null) throw new ArgumentNullException(nameof(operation)); 61 | if (maxAttempts <= 0) maxAttempts = DefaultMaxAttempts; 62 | if (initialDelay == null || initialDelay.Value < MinimumDelay) 63 | initialDelay = InitialDelay; 64 | 65 | Func> wrapped = async (token, st) => 66 | { 67 | using (var tran = stateManager.CreateTransaction()) 68 | { 69 | try 70 | { 71 | await operation(tran, cancellationToken, state); 72 | await tran.CommitAsync(); 73 | } 74 | catch (TimeoutException) 75 | { 76 | tran.Abort(); 77 | throw; 78 | } 79 | } 80 | return null; 81 | }; 82 | 83 | await ExecuteWithRetry(wrapped, state, cancellationToken, maxAttempts, initialDelay); 84 | } 85 | 86 | public static async Task ExecuteWithRetry( 87 | Func> operation, 88 | object state = null, 89 | CancellationToken cancellationToken = default(CancellationToken), 90 | int maxAttempts = DefaultMaxAttempts, 91 | TimeSpan? initialDelay = null) 92 | { 93 | if (operation == null) throw new ArgumentNullException(nameof(operation)); 94 | if (maxAttempts <= 0) maxAttempts = DefaultMaxAttempts; 95 | if (initialDelay == null || initialDelay.Value < MinimumDelay) 96 | initialDelay = InitialDelay; 97 | 98 | var result = default(TResult); 99 | for (int attempts = 0; attempts < maxAttempts; attempts++) 100 | { 101 | try 102 | { 103 | result = await operation(cancellationToken, state); 104 | break; 105 | } 106 | catch (TimeoutException) 107 | { 108 | if (attempts == DefaultMaxAttempts) 109 | { 110 | throw; 111 | } 112 | } 113 | 114 | //exponential back-off 115 | int factor = (int)Math.Pow(2, attempts) + 1; 116 | int delay = new Random(Guid.NewGuid().GetHashCode()).Next((int)(initialDelay.Value.TotalMilliseconds * 0.5D), (int)(initialDelay.Value.TotalMilliseconds * 1.5D)); 117 | await Task.Delay(factor * delay, cancellationToken); 118 | } 119 | return result; 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/ServiceFabricCacheOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using Microsoft.ServiceFabric.Services.Remoting.V2; 3 | using System; 4 | 5 | namespace SoCreate.Extensions.Caching.ServiceFabric 6 | { 7 | public class ServiceFabricCacheOptions : IOptions 8 | { 9 | public ServiceFabricCacheOptions Value => this; 10 | 11 | public Uri CacheStoreServiceUri { get; set; } 12 | public string CacheStoreEndpointName { get; set; } 13 | public Guid CacheStoreId { get; set; } 14 | public TimeSpan? RetryTimeout { get; set; } 15 | public IServiceRemotingMessageSerializationProvider SerializationProvider { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/ServiceFabricCachingServicesExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Distributed; 2 | using Microsoft.Extensions.Internal; 3 | using SoCreate.Extensions.Caching.ServiceFabric; 4 | using System; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public static class ServiceFabricCachingServicesExtensions 9 | { 10 | public static IServiceCollection AddDistributedServiceFabricCache(this IServiceCollection services, Action setupAction = null) 11 | { 12 | if (services == null) throw new ArgumentNullException(nameof(services)); 13 | 14 | if (setupAction == null) { 15 | setupAction = (s) => { }; 16 | } 17 | 18 | services.AddOptions(); 19 | services.Configure(setupAction); 20 | 21 | return services 22 | .AddSingleton() 23 | .AddSingleton() 24 | .AddSingleton(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/ServiceFabricDistributedCache.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Distributed; 2 | using Microsoft.Extensions.Internal; 3 | using Microsoft.Extensions.Options; 4 | using System; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | [assembly: InternalsVisibleTo("SoCreate.Extensions.Caching.Tests")] 10 | namespace SoCreate.Extensions.Caching.ServiceFabric 11 | { 12 | class ServiceFabricDistributedCache : IDistributedCache 13 | { 14 | private const string CacheStoreExceptionMessage = "An exception occurred while accessing cache store."; 15 | private readonly IDistributedCacheStoreLocator _distributedCacheStoreLocator; 16 | private readonly ISystemClock _systemClock; 17 | private readonly Guid _cacheStoreId; 18 | 19 | public ServiceFabricDistributedCache(IOptions options, IDistributedCacheStoreLocator distributedCacheStoreLocator, ISystemClock systemClock) 20 | { 21 | _cacheStoreId = options.Value.CacheStoreId; 22 | _distributedCacheStoreLocator = distributedCacheStoreLocator; 23 | _systemClock = systemClock; 24 | } 25 | 26 | public byte[] Get(string key) 27 | { 28 | return GetAsync(key).Result; 29 | } 30 | 31 | public async Task GetAsync(string key, CancellationToken token = default(CancellationToken)) 32 | { 33 | if (key == null) throw new ArgumentNullException(nameof(key)); 34 | 35 | key = FormatCacheKey(key); 36 | var proxy = await _distributedCacheStoreLocator.GetCacheStoreProxy(key).ConfigureAwait(false); 37 | 38 | return await ExecuteWithExceptionWrapping(async () => await proxy.GetCachedItemAsync(key).ConfigureAwait(false)); 39 | } 40 | 41 | public void Refresh(string key) 42 | { 43 | RefreshAsync(key).Wait(); 44 | } 45 | 46 | public async Task RefreshAsync(string key, CancellationToken token = default(CancellationToken)) 47 | { 48 | if (key == null) throw new ArgumentNullException(nameof(key)); 49 | 50 | await GetAsync(key, token); 51 | } 52 | 53 | public void Remove(string key) 54 | { 55 | RemoveAsync(key).Wait(); 56 | } 57 | 58 | public async Task RemoveAsync(string key, CancellationToken token = default(CancellationToken)) 59 | { 60 | if (key == null) throw new ArgumentNullException(nameof(key)); 61 | 62 | key = FormatCacheKey(key); 63 | var proxy = await _distributedCacheStoreLocator.GetCacheStoreProxy(key).ConfigureAwait(false); 64 | await ExecuteWithExceptionWrapping(async () => await proxy.RemoveCachedItemAsync(key).ConfigureAwait(false)); 65 | } 66 | 67 | public void Set(string key, byte[] value, DistributedCacheEntryOptions options) 68 | { 69 | SetAsync(key, value, options).Wait(); 70 | } 71 | 72 | public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken)) 73 | { 74 | if (key == null) throw new ArgumentNullException(nameof(key)); 75 | if (value == null) throw new ArgumentNullException(nameof(value)); 76 | 77 | var absoluteExpireTime = GetAbsoluteExpiration(_systemClock.UtcNow, options); 78 | ValidateOptions(options.SlidingExpiration, absoluteExpireTime); 79 | 80 | key = FormatCacheKey(key); 81 | var proxy = await _distributedCacheStoreLocator.GetCacheStoreProxy(key).ConfigureAwait(false); 82 | await ExecuteWithExceptionWrapping(async () => await proxy.SetCachedItemAsync(key, value, options.SlidingExpiration, absoluteExpireTime).ConfigureAwait(false)); 83 | } 84 | 85 | private DateTimeOffset? GetAbsoluteExpiration(DateTimeOffset utcNow, DistributedCacheEntryOptions options) 86 | { 87 | var expireTime = new DateTimeOffset?(); 88 | if (options.AbsoluteExpirationRelativeToNow.HasValue) 89 | expireTime = new DateTimeOffset?(utcNow.Add(options.AbsoluteExpirationRelativeToNow.Value)); 90 | else if (options.AbsoluteExpiration.HasValue) 91 | { 92 | if (options.AbsoluteExpiration.Value <= utcNow) 93 | throw new InvalidOperationException("The absolute expiration value must be in the future."); 94 | expireTime = new DateTimeOffset?(options.AbsoluteExpiration.Value); 95 | } 96 | return expireTime; 97 | } 98 | 99 | private void ValidateOptions(TimeSpan? slidingExpiration, DateTimeOffset? absoluteExpiration) 100 | { 101 | if (!slidingExpiration.HasValue && !absoluteExpiration.HasValue) 102 | throw new InvalidOperationException("Either absolute or sliding expiration needs to be provided."); 103 | } 104 | 105 | private string FormatCacheKey(string key) 106 | { 107 | return $"{_cacheStoreId}-{key}"; 108 | } 109 | 110 | private static async Task ExecuteWithExceptionWrapping(Func callback) 111 | { 112 | try 113 | { 114 | await callback().ConfigureAwait(false); 115 | } 116 | catch (Exception exception) 117 | { 118 | throw new CacheStoreException(CacheStoreExceptionMessage, exception); 119 | } 120 | } 121 | 122 | private static async Task ExecuteWithExceptionWrapping(Func> callback) 123 | { 124 | try 125 | { 126 | return await callback().ConfigureAwait(false); 127 | } 128 | catch (Exception exception) 129 | { 130 | throw new CacheStoreException(CacheStoreExceptionMessage, exception); 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/SoCreate.Extensions.Caching.ServiceFabric/SoCreate.Extensions.Caching.ServiceFabric.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | false 6 | Jami Lurock 7 | SoCreate 8 | true 9 | https://raw.githubusercontent.com/SoCreate/service-fabric-distributed-cache/master/LICENSE 10 | An implementation of the IDistributedCache that uses a Stateful Reliable Service Fabric service to act as the cache store. You can use this library to setup a distributed cache and use Service Fabric instead of Redis or SQL Server. 11 | https://github.com/SoCreate/service-fabric-distributed-cache 12 | Cache DistributedCache ServiceFabric 13 | http://service-fabric-distributed-cache.socreate.it/ 14 | git 15 | © SoCreate. All rights reserved. 16 | 3.1.0 17 | https://raw.githubusercontent.com/SoCreate/service-fabric-distributed-cache/master/assets/icon-64x64.png 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/SoCreate.Extensions.Caching.Tests/AutoMoqDataAttribute.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture; 2 | using AutoFixture.AutoMoq; 3 | using AutoFixture.Xunit2; 4 | using ServiceFabric.Mocks; 5 | 6 | namespace SoCreate.Extensions.Caching.Tests 7 | { 8 | public class AutoMoqDataAttribute : AutoDataAttribute 9 | { 10 | public AutoMoqDataAttribute() : base(() => 11 | { 12 | var fixture = new Fixture().Customize(new AutoMoqCustomization {GenerateDelegates = true}); 13 | fixture.Register(() => MockStatefulServiceContextFactory.Default); 14 | return fixture; 15 | }) 16 | { 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/SoCreate.Extensions.Caching.Tests/DistributedCacheStoreServiceTest.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture.Xunit2; 2 | using Microsoft.Extensions.Internal; 3 | using Microsoft.ServiceFabric.Data; 4 | using Microsoft.ServiceFabric.Data.Collections; 5 | using Moq; 6 | using SoCreate.Extensions.Caching.ServiceFabric; 7 | using System; 8 | using System.Collections.Generic; 9 | using System.Fabric; 10 | using System.Text; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using Xunit; 14 | 15 | namespace SoCreate.Extensions.Caching.Tests 16 | { 17 | public class DistributedCacheStoreServiceTest 18 | { 19 | [Theory, AutoMoqData] 20 | async void GetCachedItemAsync_GetItemThatExistsWithSlidingExpiration_ItemIsMovedToLastItem( 21 | [Frozen]Mock stateManager, 22 | [Frozen]Mock> cacheItemDict, 23 | [Frozen]Mock> metadataDict, 24 | [Frozen]Mock systemClock, 25 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 26 | { 27 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 28 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 29 | 30 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 31 | 32 | SetupInMemoryStores(stateManager, cacheItemDict); 33 | var metadata = SetupInMemoryStores(stateManager, metadataDict); 34 | 35 | await cacheStore.SetCachedItemAsync("mykey1", cacheValue, TimeSpan.FromSeconds(10), null); 36 | await cacheStore.SetCachedItemAsync("mykey2", cacheValue, TimeSpan.FromSeconds(10), null); 37 | await cacheStore.SetCachedItemAsync("mykey3", cacheValue, TimeSpan.FromSeconds(10), null); 38 | 39 | Assert.Equal("mykey3", metadata["CacheStoreMetadata"].LastCacheKey); 40 | 41 | await cacheStore.GetCachedItemAsync("mykey2"); 42 | 43 | Assert.Equal("mykey2", metadata["CacheStoreMetadata"].LastCacheKey); 44 | } 45 | 46 | [Theory, AutoMoqData] 47 | async void GetCachedItemAsync_GetItemThatExistsWithAbsoluteExpiration_ItemIsMovedToLastItem( 48 | [Frozen]Mock stateManager, 49 | [Frozen]Mock> cacheItemDict, 50 | [Frozen]Mock> metadataDict, 51 | [Frozen]Mock systemClock, 52 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 53 | { 54 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 55 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 56 | var expireTime = currentTime.AddSeconds(30); 57 | 58 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 59 | 60 | SetupInMemoryStores(stateManager, cacheItemDict); 61 | var metadata = SetupInMemoryStores(stateManager, metadataDict); 62 | 63 | await cacheStore.SetCachedItemAsync("mykey1", cacheValue, null, expireTime); 64 | await cacheStore.SetCachedItemAsync("mykey2", cacheValue, null, expireTime); 65 | await cacheStore.SetCachedItemAsync("mykey3", cacheValue, null, expireTime); 66 | 67 | Assert.Equal("mykey3", metadata["CacheStoreMetadata"].LastCacheKey); 68 | 69 | await cacheStore.GetCachedItemAsync("mykey2"); 70 | 71 | Assert.Equal("mykey2", metadata["CacheStoreMetadata"].LastCacheKey); 72 | } 73 | 74 | [Theory, AutoMoqData] 75 | async void GetCachedItemAsync_GetItemThatDoesNotExist_NullResultReturned( 76 | [Frozen]Mock stateManager, 77 | [Frozen]Mock> cacheItemDict, 78 | [Frozen]Mock> metadataDict, 79 | [Frozen]Mock systemClock, 80 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 81 | { 82 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 83 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 84 | var expireTime = currentTime.AddSeconds(1); 85 | 86 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 87 | 88 | SetupInMemoryStores(stateManager, metadataDict); 89 | SetupInMemoryStores(stateManager, cacheItemDict); 90 | 91 | var result = await cacheStore.GetCachedItemAsync("keyThatDoesNotExist"); 92 | Assert.Null(result); 93 | } 94 | 95 | [Theory, AutoMoqData] 96 | async void GetCachedItemAsync_GetItemThatDoesHaveKeyAndIsIsNotAbsoluteExpired_CachedItemReturned( 97 | [Frozen]Mock stateManager, 98 | [Frozen]Mock> cacheItemDict, 99 | [Frozen]Mock> metadataDict, 100 | [Frozen]Mock systemClock, 101 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 102 | { 103 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 104 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 105 | var expireTime = currentTime.AddSeconds(1); 106 | 107 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 108 | 109 | SetupInMemoryStores(stateManager, metadataDict); 110 | SetupInMemoryStores(stateManager, cacheItemDict); 111 | 112 | await cacheStore.SetCachedItemAsync("mykey", cacheValue, null, expireTime); 113 | var result = await cacheStore.GetCachedItemAsync("mykey"); 114 | Assert.Equal(cacheValue, result); 115 | } 116 | 117 | [Theory, AutoMoqData] 118 | async void GetCachedItemAsync_GetItemThatDoesHaveKeyAndIsIsAbsoluteExpired_NullResultReturned( 119 | [Frozen]Mock stateManager, 120 | [Frozen]Mock> cacheItemDict, 121 | [Frozen]Mock> metadataDict, 122 | [Frozen]Mock systemClock, 123 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 124 | { 125 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 126 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 127 | var expireTime = currentTime.AddSeconds(-1); 128 | 129 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 130 | 131 | SetupInMemoryStores(stateManager, metadataDict); 132 | SetupInMemoryStores(stateManager, cacheItemDict); 133 | 134 | await cacheStore.SetCachedItemAsync("mykey", cacheValue, null, expireTime); 135 | var result = await cacheStore.GetCachedItemAsync("mykey"); 136 | Assert.Null(result); 137 | } 138 | 139 | [Theory, AutoMoqData] 140 | async void GetCachedItemAsync_GetItemThatDoesHaveKeyAndIsIsAbsoluteExpiredDoesNotSlideTime_ExpireTimeDoesNotSlide( 141 | [Frozen]Mock stateManager, 142 | [Frozen]Mock> cacheItemDict, 143 | [Frozen]Mock> metadataDict, 144 | [Frozen]Mock systemClock, 145 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 146 | { 147 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 148 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 149 | var expireTime = currentTime.AddSeconds(5); 150 | 151 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 152 | 153 | SetupInMemoryStores(stateManager, metadataDict); 154 | SetupInMemoryStores(stateManager, cacheItemDict); 155 | 156 | await cacheStore.SetCachedItemAsync("mykey", cacheValue, null, expireTime); 157 | var result = await cacheStore.GetCachedItemAsync("mykey"); 158 | Assert.Equal(cacheValue, result); 159 | 160 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime.AddSeconds(5)); 161 | 162 | var resultAfter6Seconds = await cacheStore.GetCachedItemAsync("mykey"); 163 | Assert.Null(resultAfter6Seconds); 164 | } 165 | 166 | [Theory, AutoMoqData] 167 | async void GetCachedItemAsync_GetItemThatDoesHaveKeyAndIsIsNotSlidingExpired_CachedItemReturned( 168 | [Frozen]Mock stateManager, 169 | [Frozen]Mock> cacheItemDict, 170 | [Frozen]Mock> metadataDict, 171 | [Frozen]Mock systemClock, 172 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 173 | { 174 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 175 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 176 | 177 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 178 | 179 | SetupInMemoryStores(stateManager, metadataDict); 180 | SetupInMemoryStores(stateManager, cacheItemDict); 181 | 182 | await cacheStore.SetCachedItemAsync("mykey", cacheValue, TimeSpan.FromSeconds(1), null); 183 | var result = await cacheStore.GetCachedItemAsync("mykey"); 184 | Assert.Equal(cacheValue, result); 185 | } 186 | 187 | 188 | [Theory, AutoMoqData] 189 | async void GetCachedItemAsync_GetItemThatDoesHaveKeyAndIsIsSlidingExpired_NullResultReturned( 190 | [Frozen]Mock stateManager, 191 | [Frozen]Mock> cacheItemDict, 192 | [Frozen]Mock> metadataDict, 193 | [Frozen]Mock systemClock, 194 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 195 | { 196 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 197 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 198 | 199 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 200 | 201 | SetupInMemoryStores(stateManager, metadataDict); 202 | SetupInMemoryStores(stateManager, cacheItemDict); 203 | 204 | await cacheStore.SetCachedItemAsync("mykey", cacheValue, TimeSpan.FromSeconds(1), null); 205 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime.AddSeconds(2)); 206 | var result = await cacheStore.GetCachedItemAsync("mykey"); 207 | Assert.Null(result); 208 | } 209 | 210 | [Theory, AutoMoqData] 211 | async void GetCachedItemAsync_GetItemThatDoesHaveKeyAndIsIsSlidingExpired_SlidedExpirationUpdates( 212 | [Frozen]Mock stateManager, 213 | [Frozen]Mock> cacheItemDict, 214 | [Frozen]Mock> metadataDict, 215 | [Frozen]Mock systemClock, 216 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 217 | { 218 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 219 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 220 | 221 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 222 | 223 | SetupInMemoryStores(stateManager, cacheItemDict); 224 | SetupInMemoryStores(stateManager, metadataDict); 225 | 226 | await cacheStore.SetCachedItemAsync("mykey", cacheValue, TimeSpan.FromSeconds(10), null); 227 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime.AddSeconds(5)); 228 | var resultAfter5Seconds = await cacheStore.GetCachedItemAsync("mykey"); 229 | Assert.Equal(cacheValue, resultAfter5Seconds); 230 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime.AddSeconds(8)); 231 | var resultAfter8Seconds = await cacheStore.GetCachedItemAsync("mykey"); 232 | Assert.Equal(cacheValue, resultAfter8Seconds); 233 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime.AddSeconds(9)); 234 | var resultAfter9Seconds = await cacheStore.GetCachedItemAsync("mykey"); 235 | Assert.Equal(cacheValue, resultAfter9Seconds); 236 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime.AddSeconds(19)); 237 | var resultAfter19Seconds = await cacheStore.GetCachedItemAsync("mykey"); 238 | Assert.Null(resultAfter19Seconds); 239 | } 240 | 241 | [Theory, AutoMoqData] 242 | async void SetCachedItemAsync_AddItemsToCreateLinkedDictionary_DictionaryCreatedWithItemsLinked( 243 | [Frozen]Mock stateManager, 244 | [Frozen]Mock> cacheItemDict, 245 | [Frozen]Mock> metadataDict, 246 | [Frozen]Mock systemClock, 247 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 248 | { 249 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 250 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 251 | 252 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 253 | 254 | var cachedItems = SetupInMemoryStores(stateManager, cacheItemDict); 255 | var metadata = SetupInMemoryStores(stateManager, metadataDict); 256 | 257 | await cacheStore.SetCachedItemAsync("1", cacheValue, TimeSpan.FromSeconds(10), null); 258 | await cacheStore.SetCachedItemAsync("2", cacheValue, TimeSpan.FromSeconds(10), null); 259 | await cacheStore.SetCachedItemAsync("3", cacheValue, TimeSpan.FromSeconds(10), null); 260 | await cacheStore.SetCachedItemAsync("4", cacheValue, TimeSpan.FromSeconds(10), null); 261 | 262 | Assert.Null(cachedItems["1"].BeforeCacheKey); 263 | foreach (var item in cachedItems) 264 | { 265 | if (item.Value.BeforeCacheKey != null) 266 | { 267 | Assert.Equal(item.Key, cachedItems[item.Value.BeforeCacheKey].AfterCacheKey); 268 | } 269 | if (item.Value.AfterCacheKey != null) 270 | { 271 | Assert.Equal(item.Key, cachedItems[item.Value.AfterCacheKey].BeforeCacheKey); 272 | } 273 | } 274 | Assert.Null(cachedItems["4"].AfterCacheKey); 275 | 276 | Assert.Equal("1", metadata["CacheStoreMetadata"].FirstCacheKey); 277 | Assert.Equal("4", metadata["CacheStoreMetadata"].LastCacheKey); 278 | Assert.Equal((cacheValue.Length + 250) * cachedItems.Count, metadata["CacheStoreMetadata"].Size); 279 | } 280 | 281 | [Theory, AutoMoqData] 282 | async void RemoveCachedItemAsync_RemoveItemsFromLinkedDictionary_ListStaysLinkedTogetherAfterItemsRemoved( 283 | [Frozen]Mock stateManager, 284 | [Frozen]Mock> cacheItemDict, 285 | [Frozen]Mock> metadataDict, 286 | [Frozen]Mock systemClock, 287 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 288 | { 289 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 290 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 291 | 292 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 293 | 294 | var cachedItems = SetupInMemoryStores(stateManager, cacheItemDict); 295 | var metadata = SetupInMemoryStores(stateManager, metadataDict); 296 | 297 | await cacheStore.SetCachedItemAsync("1", cacheValue, TimeSpan.FromSeconds(10), null); 298 | await cacheStore.SetCachedItemAsync("2", cacheValue, TimeSpan.FromSeconds(10), null); 299 | await cacheStore.SetCachedItemAsync("3", cacheValue, TimeSpan.FromSeconds(10), null); 300 | await cacheStore.SetCachedItemAsync("4", cacheValue, TimeSpan.FromSeconds(10), null); 301 | await cacheStore.SetCachedItemAsync("5", cacheValue, TimeSpan.FromSeconds(10), null); 302 | await cacheStore.SetCachedItemAsync("6", cacheValue, TimeSpan.FromSeconds(10), null); 303 | await cacheStore.SetCachedItemAsync("7", cacheValue, TimeSpan.FromSeconds(10), null); 304 | await cacheStore.SetCachedItemAsync("8", cacheValue, TimeSpan.FromSeconds(10), null); 305 | 306 | await cacheStore.RemoveCachedItemAsync("3"); 307 | await cacheStore.RemoveCachedItemAsync("4"); 308 | await cacheStore.RemoveCachedItemAsync("8"); 309 | await cacheStore.RemoveCachedItemAsync("1"); 310 | 311 | Assert.Null(cachedItems["2"].BeforeCacheKey); 312 | foreach (var item in cachedItems) 313 | { 314 | if (item.Value.BeforeCacheKey != null) 315 | { 316 | Assert.Equal(item.Key, cachedItems[item.Value.BeforeCacheKey].AfterCacheKey); 317 | } 318 | if (item.Value.AfterCacheKey != null) 319 | { 320 | Assert.Equal(item.Key, cachedItems[item.Value.AfterCacheKey].BeforeCacheKey); 321 | } 322 | } 323 | Assert.Null(cachedItems["7"].AfterCacheKey); 324 | 325 | Assert.Equal("2", metadata["CacheStoreMetadata"].FirstCacheKey); 326 | Assert.Equal("7", metadata["CacheStoreMetadata"].LastCacheKey); 327 | Assert.Equal((cacheValue.Length + 250) * cachedItems.Count, metadata["CacheStoreMetadata"].Size); 328 | } 329 | 330 | [Theory, AutoMoqData] 331 | async void RemoveCachedItemAsync_RemoveItemsFromLinkedDictionary_RemovalWorksWithMalformedMetadata( 332 | [Frozen]Mock stateManager, 333 | [Frozen]Mock> cacheItemDict, 334 | [Frozen]Mock> metadataDict, 335 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 336 | { 337 | var cacheValue = Encoding.UTF8.GetBytes("someValue"); 338 | 339 | var cachedItems = SetupInMemoryStores(stateManager, cacheItemDict); 340 | var metadata = SetupInMemoryStores(stateManager, metadataDict); 341 | 342 | await cacheStore.SetCachedItemAsync("1", cacheValue, TimeSpan.FromSeconds(10), null); 343 | await cacheStore.SetCachedItemAsync("2", cacheValue, TimeSpan.FromSeconds(10), null); 344 | await cacheStore.SetCachedItemAsync("3", cacheValue, TimeSpan.FromSeconds(10), null); 345 | 346 | metadata["CacheStoreMetadata"] = new CacheStoreMetadata(int.MaxValue, null, "3"); 347 | await cacheStore.RemoveLeastRecentlyUsedCacheItemWhenOverMaxCacheSize(); 348 | 349 | metadata["CacheStoreMetadata"] = new CacheStoreMetadata(int.MaxValue, "Garbage", "3"); 350 | await cacheStore.RemoveLeastRecentlyUsedCacheItemWhenOverMaxCacheSize(); 351 | } 352 | 353 | [Theory, AutoMoqData] 354 | async void RemoveLeastRecentlyUsedCacheItemWhenOverMaxCacheSize_RemoveItemsFromLinkedDictionary_DoesNotRemoveNonExpiredItems( 355 | [Frozen]Mock stateManager, 356 | [Frozen]Mock> cacheItemDict, 357 | [Frozen]Mock> metadataDict, 358 | [Frozen]Mock systemClock, 359 | [Greedy]ServiceFabricDistributedCacheStoreService cacheStore) 360 | { 361 | var cacheValue = new byte[1000000]; 362 | var currentTime = new DateTime(2019, 2, 1, 1, 0, 0); 363 | 364 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime); 365 | 366 | var cachedItems = SetupInMemoryStores(stateManager, cacheItemDict); 367 | var metadata = SetupInMemoryStores(stateManager, metadataDict); 368 | 369 | await cacheStore.SetCachedItemAsync("1", cacheValue, TimeSpan.FromMinutes(10), null); 370 | for (var i = 2; i <= 10; i++) 371 | { 372 | await cacheStore.SetCachedItemAsync(i.ToString(), cacheValue, TimeSpan.FromSeconds(10), null); 373 | } 374 | 375 | systemClock.SetupGet(m => m.UtcNow).Returns(currentTime.AddSeconds(10)); 376 | await cacheStore.RemoveLeastRecentlyUsedCacheItemWhenOverMaxCacheSize(); 377 | 378 | Assert.Single(cachedItems); 379 | Assert.Equal("1", metadata["CacheStoreMetadata"].FirstCacheKey); 380 | Assert.Equal("1", metadata["CacheStoreMetadata"].LastCacheKey); 381 | } 382 | 383 | 384 | private Dictionary SetupInMemoryStores(Mock stateManager, Mock> reliableDict) where TKey : IComparable, IEquatable 385 | { 386 | var inMemoryDict = new Dictionary(); 387 | Func> getItem = (key) => inMemoryDict.ContainsKey(key) ? new ConditionalValue(true, inMemoryDict[key]) : new ConditionalValue(false, default(TValue)); 388 | 389 | stateManager.Setup(m => m.GetOrAddAsync>(It.IsAny())).Returns(Task.FromResult(reliableDict.Object)); 390 | reliableDict.Setup(m => m.TryGetValueAsync(It.IsAny(), It.IsAny())).Returns((ITransaction t, TKey key) => Task.FromResult(getItem(key))); 391 | reliableDict.Setup(m => m.TryGetValueAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns((ITransaction t, TKey key, LockMode l) => Task.FromResult(getItem(key))); 392 | reliableDict.Setup(m => m.SetAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns((ITransaction t, TKey key, TValue ci) => { inMemoryDict[key] = ci; return Task.CompletedTask; }); 393 | reliableDict.Setup(m => m.TryRemoveAsync(It.IsAny(), It.IsAny())).Returns((ITransaction t, TKey key) => { var r = getItem(key); inMemoryDict.Remove(key); return Task.FromResult(r); }); 394 | 395 | return inMemoryDict; 396 | } 397 | 398 | class ServiceFabricDistributedCacheStoreService : DistributedCacheStoreService 399 | { 400 | public ServiceFabricDistributedCacheStoreService(StatefulServiceContext context, IReliableStateManagerReplica2 replica, ISystemClock clock) : base(context, replica, clock, (m) => { }) 401 | { 402 | } 403 | 404 | protected override int MaxCacheSizeInMegabytes => 1; 405 | 406 | public async Task RemoveLeastRecentlyUsedCacheItemWhenOverMaxCacheSize() 407 | { 408 | await RemoveLeastRecentlyUsedCacheItemWhenOverMaxCacheSize(CancellationToken.None); 409 | } 410 | } 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /tests/SoCreate.Extensions.Caching.Tests/LinkedDictionaryHelperTest.cs: -------------------------------------------------------------------------------- 1 | using AutoFixture.Xunit2; 2 | using Microsoft.ServiceFabric.Data; 3 | using Moq; 4 | using SoCreate.Extensions.Caching.ServiceFabric; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace SoCreate.Extensions.Caching.Tests 12 | { 13 | public class LinkedDictionaryHelperTest 14 | { 15 | [Theory, AutoMoqData] 16 | async void AddLast_AddNewItemToEndOfList_LinkOldLastItemToNewLastItem( 17 | [Frozen]Mock>>> getCacheItem, 18 | CacheStoreMetadata cacheStoreMetadata, 19 | ConditionalValue cachedItem, 20 | CachedItem newCachedItem, 21 | LinkedDictionaryHelper linkedDictionaryHelper) 22 | { 23 | var newItemKey = "NewLastItem"; 24 | var cachedValue = Encoding.UTF8.GetBytes("some value"); 25 | var totalSize = cacheStoreMetadata.Size + cachedValue.Length; 26 | 27 | getCacheItem.Setup(mock => mock(It.IsAny())).ReturnsAsync(await Task.FromResult(cachedItem)); 28 | 29 | var result = await linkedDictionaryHelper.AddLast(cacheStoreMetadata, newItemKey, newCachedItem, cachedValue); 30 | Assert.Equal(2, result.CachedItemsToUpdate.Count); 31 | 32 | var oldLastItem = result.CachedItemsToUpdate[result.CachedItemsToUpdate[newItemKey].BeforeCacheKey]; 33 | Assert.Equal(newItemKey, oldLastItem.AfterCacheKey); 34 | Assert.Equal(cachedItem.Value.Value, oldLastItem.Value); 35 | Assert.Equal(cachedItem.Value.SlidingExpiration, oldLastItem.SlidingExpiration); 36 | Assert.Equal(cachedItem.Value.AbsoluteExpiration, oldLastItem.AbsoluteExpiration); 37 | 38 | var newLastItem = result.CachedItemsToUpdate[newItemKey]; 39 | 40 | Assert.Equal(cacheStoreMetadata.LastCacheKey, newLastItem.BeforeCacheKey); 41 | Assert.Null(newLastItem.AfterCacheKey); 42 | Assert.Equal(cachedValue, newLastItem.Value); 43 | Assert.Equal(newCachedItem.SlidingExpiration, newLastItem.SlidingExpiration); 44 | Assert.Equal(newCachedItem.AbsoluteExpiration, newLastItem.AbsoluteExpiration); 45 | Assert.Equal(totalSize, result.CacheStoreMetadata.Size); 46 | Assert.Equal(cacheStoreMetadata.FirstCacheKey, result.CacheStoreMetadata.FirstCacheKey); 47 | Assert.Equal(newItemKey, result.CacheStoreMetadata.LastCacheKey); 48 | } 49 | 50 | [Theory, AutoMoqData] 51 | async void AddLast_AddNewItemToEndOfEmptyList_ListContainsOnlyNewItem( 52 | [Frozen]Mock>>> getCacheItem, 53 | ConditionalValue cachedItem, 54 | CachedItem newCachedItem, 55 | LinkedDictionaryHelper linkedDictionaryHelper) 56 | { 57 | var cacheStoreMetadata = new CacheStoreMetadata(0, null, null); 58 | var newItemKey = "NewLastItem"; 59 | var cachedValue = Encoding.UTF8.GetBytes("some value"); 60 | var totalSize = cacheStoreMetadata.Size + cachedValue.Length; 61 | 62 | getCacheItem.Setup(mock => mock(It.IsAny())).ReturnsAsync(await Task.FromResult(cachedItem)); 63 | 64 | var result = await linkedDictionaryHelper.AddLast(cacheStoreMetadata, newItemKey, newCachedItem, cachedValue); 65 | Assert.Equal(1, result.CachedItemsToUpdate.Count); 66 | 67 | var newLastItem = result.CachedItemsToUpdate[newItemKey]; 68 | 69 | Assert.Null(newLastItem.BeforeCacheKey); 70 | Assert.Null(newLastItem.AfterCacheKey); 71 | Assert.Equal(cachedValue, newLastItem.Value); 72 | Assert.Equal(newCachedItem.SlidingExpiration, newLastItem.SlidingExpiration); 73 | Assert.Equal(newCachedItem.AbsoluteExpiration, newLastItem.AbsoluteExpiration); 74 | Assert.Equal(totalSize, result.CacheStoreMetadata.Size); 75 | Assert.Equal(newItemKey, result.CacheStoreMetadata.FirstCacheKey); 76 | Assert.Equal(newItemKey, result.CacheStoreMetadata.LastCacheKey); 77 | } 78 | 79 | [Theory, AutoMoqData] 80 | async void Remove_OnlyItemInLinkedDictionary_SetCacheItemNotCalled( 81 | CacheStoreMetadata cacheStoreMetadata, 82 | LinkedDictionaryHelper linkedDictionaryHelper) 83 | { 84 | var cachedValue = Encoding.UTF8.GetBytes("some value"); 85 | var totalSize = cacheStoreMetadata.Size - cachedValue.Length; 86 | var c = new CachedItem(cachedValue, null, null); 87 | 88 | var result = await linkedDictionaryHelper.Remove(cacheStoreMetadata, c); 89 | Assert.Empty(result.CachedItemsToUpdate); 90 | 91 | Assert.Equal(totalSize, result.CacheStoreMetadata.Size); 92 | Assert.Null(result.CacheStoreMetadata.FirstCacheKey); 93 | Assert.Null(result.CacheStoreMetadata.LastCacheKey); 94 | } 95 | 96 | [Theory, AutoMoqData] 97 | async void Remove_FirstItemInLinkedDictionary_SetSecondItemToBeFirst( 98 | [Frozen]Mock>>> getCacheItem, 99 | CacheStoreMetadata cacheStoreMetadata, 100 | LinkedDictionaryHelper linkedDictionaryHelper) 101 | { 102 | var cachedValue = Encoding.UTF8.GetBytes("some value"); 103 | var totalSize = cacheStoreMetadata.Size - cachedValue.Length; 104 | 105 | var items = new Dictionary { 106 | { "1", new CachedItem(cachedValue, null, "2") }, 107 | { "2", new CachedItem(cachedValue, "1", "3", TimeSpan.FromMilliseconds(100), DateTimeOffset.MinValue) }, 108 | { "3", new CachedItem(cachedValue, "2", null) } 109 | }; 110 | 111 | getCacheItem.Setup(mock => mock(It.IsAny())).ReturnsAsync(await Task.FromResult(new ConditionalValue(true, items["2"]))); 112 | 113 | var result = await linkedDictionaryHelper.Remove(cacheStoreMetadata, items["1"]); 114 | Assert.Single(result.CachedItemsToUpdate); 115 | var newFirstItem = result.CachedItemsToUpdate["2"]; 116 | 117 | Assert.Null(newFirstItem.BeforeCacheKey); 118 | Assert.Equal(items["2"].AfterCacheKey, newFirstItem.AfterCacheKey); 119 | Assert.Equal(cachedValue, newFirstItem.Value); 120 | Assert.Equal(items["2"].SlidingExpiration, newFirstItem.SlidingExpiration); 121 | Assert.Equal(items["2"].AbsoluteExpiration, newFirstItem.AbsoluteExpiration); 122 | 123 | Assert.Equal(totalSize, result.CacheStoreMetadata.Size); 124 | Assert.Equal("2", result.CacheStoreMetadata.FirstCacheKey); 125 | Assert.Equal(cacheStoreMetadata.LastCacheKey, result.CacheStoreMetadata.LastCacheKey); 126 | } 127 | 128 | [Theory, AutoMoqData] 129 | async void Remove_LastItemInLinkedDictionary_SetSecondItemFromLastToBeLast( 130 | [Frozen]Mock>>> getCacheItem, 131 | CacheStoreMetadata cacheStoreMetadata, 132 | LinkedDictionaryHelper linkedDictionaryHelper) 133 | { 134 | var cachedValue = Encoding.UTF8.GetBytes("some value"); 135 | var totalSize = cacheStoreMetadata.Size - cachedValue.Length; 136 | 137 | var items = new Dictionary { 138 | { "1", new CachedItem(cachedValue, null, "2") }, 139 | { "2", new CachedItem(cachedValue, "1", "3", TimeSpan.FromMilliseconds(100), DateTimeOffset.MinValue) }, 140 | { "3", new CachedItem(cachedValue, "2", null) } 141 | }; 142 | 143 | getCacheItem.Setup(mock => mock(It.IsAny())).ReturnsAsync(await Task.FromResult(new ConditionalValue(true, items["2"]))); 144 | 145 | var result = await linkedDictionaryHelper.Remove(cacheStoreMetadata, items["3"]); 146 | Assert.Single(result.CachedItemsToUpdate); 147 | var newLastItem = result.CachedItemsToUpdate["2"]; 148 | 149 | Assert.Equal(items["2"].BeforeCacheKey, newLastItem.BeforeCacheKey); 150 | Assert.Null(newLastItem.AfterCacheKey); 151 | Assert.Equal(cachedValue, newLastItem.Value); 152 | Assert.Equal(items["2"].SlidingExpiration, newLastItem.SlidingExpiration); 153 | Assert.Equal(items["2"].AbsoluteExpiration, newLastItem.AbsoluteExpiration); 154 | 155 | Assert.Equal(totalSize, result.CacheStoreMetadata.Size); 156 | Assert.Equal(cacheStoreMetadata.FirstCacheKey, result.CacheStoreMetadata.FirstCacheKey); 157 | Assert.Equal("2", result.CacheStoreMetadata.LastCacheKey); 158 | } 159 | 160 | [Theory, AutoMoqData] 161 | async void Remove_MiddleItemInLinkedDictionary_ItemBeforeAndAfterNeedTobeLinked( 162 | [Frozen]Mock>>> getCacheItem, 163 | CacheStoreMetadata cacheStoreMetadata, 164 | LinkedDictionaryHelper linkedDictionaryHelper) 165 | { 166 | var cachedValue = Encoding.UTF8.GetBytes("some value"); 167 | var totalSize = cacheStoreMetadata.Size - cachedValue.Length; 168 | 169 | var items = new Dictionary { 170 | { "1", new CachedItem(cachedValue, null, "2", TimeSpan.FromMilliseconds(10), DateTimeOffset.MinValue) }, 171 | { "2", new CachedItem(cachedValue, "1", "3") }, 172 | { "3", new CachedItem(cachedValue, "2", null, TimeSpan.FromMilliseconds(100), DateTimeOffset.MinValue) } 173 | }; 174 | 175 | getCacheItem.Setup(mock => mock("1")).ReturnsAsync(await Task.FromResult(new ConditionalValue(true, items["1"]))); 176 | getCacheItem.Setup(mock => mock("3")).ReturnsAsync(await Task.FromResult(new ConditionalValue(true, items["3"]))); 177 | 178 | 179 | var result = await linkedDictionaryHelper.Remove(cacheStoreMetadata, items["2"]); 180 | Assert.Equal(2, result.CachedItemsToUpdate.Count); 181 | 182 | var newFirstItem = result.CachedItemsToUpdate["1"]; 183 | Assert.Null(newFirstItem.BeforeCacheKey); 184 | Assert.Equal("3", newFirstItem.AfterCacheKey); 185 | Assert.Equal(cachedValue, newFirstItem.Value); 186 | Assert.Equal(items["1"].SlidingExpiration, newFirstItem.SlidingExpiration); 187 | Assert.Equal(items["1"].AbsoluteExpiration, newFirstItem.AbsoluteExpiration); 188 | 189 | var newLastItem = result.CachedItemsToUpdate["3"]; 190 | Assert.Equal("1", newLastItem.BeforeCacheKey); 191 | Assert.Null(newLastItem.AfterCacheKey); 192 | Assert.Equal(cachedValue, newLastItem.Value); 193 | Assert.Equal(items["3"].SlidingExpiration, newLastItem.SlidingExpiration); 194 | Assert.Equal(items["3"].AbsoluteExpiration, newLastItem.AbsoluteExpiration); 195 | 196 | Assert.Equal(totalSize, result.CacheStoreMetadata.Size); 197 | Assert.Equal(cacheStoreMetadata.FirstCacheKey, result.CacheStoreMetadata.FirstCacheKey); 198 | Assert.Equal(cacheStoreMetadata.LastCacheKey, result.CacheStoreMetadata.LastCacheKey); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /tests/SoCreate.Extensions.Caching.Tests/SoCreate.Extensions.Caching.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | win7-x64 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | --------------------------------------------------------------------------------