├── .gitattributes ├── .gitignore ├── ConsoleBenchmark ├── ConsoleBenchmark.csproj └── Program.cs ├── Net.DistributedFileStoreCache.sln ├── Net.DistributedFileStoreCache ├── DistributedFileStoreCacheBytes.cs ├── DistributedFileStoreCacheClass.cs ├── DistributedFileStoreCacheNuGetIcon.png ├── DistributedFileStoreCacheOptions.cs ├── DistributedFileStoreCacheString.cs ├── IDistributedFileStoreCacheBytes.cs ├── IDistributedFileStoreCacheClass.cs ├── IDistributedFileStoreCacheString.cs ├── Net.DistributedFileStoreCache.csproj ├── RegisterDistributedFileStoreCache.cs └── SupportCode │ ├── CacheFileExtensions.cs │ ├── CacheFileHandler.cs │ ├── CacheFileRemove.cs │ ├── CacheFileSetMany.cs │ ├── CacheFileSetOne.cs │ ├── CacheJsonContent.cs │ ├── DistributedFileStoreCacheException.cs │ ├── ExpirationExtensions.cs │ ├── HandleUnauthorizedAccess.cs │ ├── StaticCachePart.cs │ └── ValueTaskSyncCheckers.cs ├── README.md ├── ReleaseNotes.md └── Test ├── Test.csproj ├── TestData ├── FileStoreCacheFile.TestCacheServiceParallel.json ├── FileStoreCacheFile.TestDistributedFileStoreCacheClass.json ├── FileStoreCacheFile.TestDistributedFileStoreCacheClassAsync.json ├── FileStoreCacheFile.TestDistributedFileStoreCacheString.json ├── FileStoreCacheFile.TestDistributedFileStoreCacheString_Async.json ├── FileStoreCacheFile.TestIDistributedFileStoreCacheBytes.json ├── FileStoreCacheFile.TestJsonSerializerOptions.json ├── FileStoreCacheFile.TestMaxBytes.json ├── FileStoreCacheFile.TestStaticCachePart.json ├── TestJsonSerializerStream.json ├── testlock.2.json └── testlock.json ├── TestHelpers ├── DisplayExtensions.cs ├── ParallelExtensions.cs ├── StubFileStoreCacheClass.cs └── StubFileStoreCacheString.cs ├── UnitTests ├── TestCacheFileExtensions.cs ├── TestCacheServiceParallel.cs ├── TestDistributedFileStoreCacheClass.cs ├── TestDistributedFileStoreCacheClassAsync.cs ├── TestDistributedFileStoreCacheString.cs ├── TestDistributedFileStoreCacheString_Async.cs ├── TestFileLock.cs ├── TestIDistributedFileStoreCacheBytes.cs ├── TestJsonSerializerOptions.cs ├── TestJsonSerializerStream.cs ├── TestMaxBytes.cs ├── TestSqlServerTiming.cs └── TestStaticCachePart.cs └── appsettings.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.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 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /ConsoleBenchmark/ConsoleBenchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /ConsoleBenchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using BenchmarkDotNet.Attributes; 2 | using BenchmarkDotNet.Running; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Net.DistributedFileStoreCache; 5 | using TestSupport.Helpers; 6 | 7 | namespace ConsoleBenchmark; 8 | 9 | public class ConsoleBenchmark 10 | { 11 | private readonly IDistributedFileStoreCacheString _distributedCache; 12 | 13 | public ConsoleBenchmark() 14 | { 15 | var services = new ServiceCollection(); 16 | services.AddDistributedFileStoreCache(options => 17 | { 18 | options.WhichVersion = FileStoreCacheVersions.String; 19 | options.PathToCacheFileDirectory = TestData.GetCallingAssemblyTopLevelDir(); 20 | options.SecondPartOfCacheFileName = GetType().Name; 21 | options.MaxBytesInJsonCacheFile = 50 * 10000; 22 | }); 23 | var serviceProvider = services.BuildServiceProvider(); 24 | 25 | _distributedCache = serviceProvider.GetRequiredService(); 26 | } 27 | 28 | //[Params(10_000)] 29 | [Params(100, 1000, 10_000)] 30 | public int NumKeysAtStart { get; set; } 31 | 32 | [GlobalSetup] 33 | public void GlobalSetup() 34 | { 35 | var allKeyValues = new List>(); 36 | for (int i = 0; i < NumKeysAtStart; i++) 37 | { 38 | allKeyValues.Add(new KeyValuePair($"Key{i:D4}", DateTime.UtcNow.ToString("O"))); 39 | } 40 | _distributedCache.ClearAll(allKeyValues); 41 | } 42 | 43 | [Benchmark] 44 | public void AddKey() 45 | { 46 | _distributedCache.Set("NewKey", DateTime.UtcNow.ToString("O"), null); 47 | _distributedCache.Get("NewKey"); //This forces an read 48 | } 49 | 50 | [Benchmark] 51 | public void AddManyKey100() 52 | { 53 | var allKeyValues = new List>(); 54 | for (int i = 0; i < 100; i++) 55 | { 56 | allKeyValues.Add(new KeyValuePair($"NewKey{i:D4}", DateTime.UtcNow.ToString("O"))); 57 | } 58 | 59 | _distributedCache.SetMany(allKeyValues); 60 | _distributedCache.Get("NewKey"); //This forces an read 61 | } 62 | 63 | [Benchmark] 64 | public async Task AddKeyAsync() 65 | { 66 | await _distributedCache.SetAsync("NewKey", DateTime.UtcNow.ToString("O"), null); 67 | await _distributedCache.GetAsync("NewKey"); //This forces an read 68 | } 69 | 70 | [Benchmark] 71 | public void GetKey() 72 | { 73 | _distributedCache.Get("Key0000"); 74 | } 75 | 76 | [Benchmark] 77 | public void GetAllKeyValues() 78 | { 79 | var all = _distributedCache.GetAllKeyValues(); 80 | } 81 | } 82 | 83 | public class Program 84 | { 85 | public static void Main(string[] args) 86 | { 87 | var summary = BenchmarkRunner.Run(typeof(Program).Assembly); 88 | } 89 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32414.318 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Net.DistributedFileStoreCache", "Net.DistributedFileStoreCache\Net.DistributedFileStoreCache.csproj", "{EF4587A9-4F55-426C-AB6C-E170B7EB40A4}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "Test\Test.csproj", "{2EC16B67-B934-4ED2-A432-09F6D4041613}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleBenchmark", "ConsoleBenchmark\ConsoleBenchmark.csproj", "{AF40BC6A-932C-474B-8AED-F56778767A1C}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8CC4585E-0547-4323-BC2F-9096E968A2FB}" 13 | ProjectSection(SolutionItems) = preProject 14 | README.md = README.md 15 | ReleaseNotes.md = ReleaseNotes.md 16 | EndProjectSection 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {EF4587A9-4F55-426C-AB6C-E170B7EB40A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {EF4587A9-4F55-426C-AB6C-E170B7EB40A4}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {EF4587A9-4F55-426C-AB6C-E170B7EB40A4}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {EF4587A9-4F55-426C-AB6C-E170B7EB40A4}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {2EC16B67-B934-4ED2-A432-09F6D4041613}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {2EC16B67-B934-4ED2-A432-09F6D4041613}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {2EC16B67-B934-4ED2-A432-09F6D4041613}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {2EC16B67-B934-4ED2-A432-09F6D4041613}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {AF40BC6A-932C-474B-8AED-F56778767A1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {AF40BC6A-932C-474B-8AED-F56778767A1C}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {AF40BC6A-932C-474B-8AED-F56778767A1C}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {AF40BC6A-932C-474B-8AED-F56778767A1C}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {92FE382C-E339-4AAB-9E5B-3DA1CB792DAF} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/DistributedFileStoreCacheBytes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text; 5 | using Microsoft.Extensions.Caching.Distributed; 6 | 7 | namespace Net.DistributedFileStoreCache; 8 | 9 | /// 10 | /// This is the Distributed FileStore cache that has a value of type byte[] 11 | /// 12 | public class DistributedFileStoreCacheBytes : IDistributedFileStoreCacheBytes 13 | { 14 | private readonly IDistributedFileStoreCacheString _stringCache; 15 | 16 | /// 17 | /// ctor 18 | /// 19 | /// 20 | public DistributedFileStoreCacheBytes(IDistributedFileStoreCacheString stringCache) 21 | { 22 | _stringCache = stringCache; 23 | } 24 | 25 | /// Gets a value with the given key. 26 | /// A string identifying the requested value. 27 | /// The located value or null. 28 | public byte[]? Get(string key) 29 | { 30 | var stringValue = _stringCache.Get(key); 31 | if (stringValue == null) 32 | return null; 33 | return Encoding.UTF8.GetBytes(stringValue); 34 | } 35 | 36 | /// Gets a value with the given key. 37 | /// A string identifying the requested value. 38 | /// Optional. The used to propagate notifications that the operation should be canceled. 39 | /// The that represents the asynchronous operation, containing the located value or null. 40 | public async Task GetAsync(string key, CancellationToken token = new CancellationToken()) 41 | { 42 | var stringValue = await _stringCache.GetAsync(key, token); 43 | if (stringValue == null) 44 | return null; 45 | return Encoding.UTF8.GetBytes(stringValue); 46 | } 47 | 48 | /// Sets a value with the given key. 49 | /// A string identifying the requested value. 50 | /// The value to set in the cache. 51 | /// The cache options for the value. 52 | public void Set(string key, byte[] value, DistributedCacheEntryOptions? options) 53 | { 54 | if (value == null) throw new ArgumentNullException(nameof(value)); 55 | _stringCache.Set(key, Encoding.UTF8.GetString(value), options); 56 | } 57 | 58 | /// Sets the value with the given key. 59 | /// A string identifying the requested value. 60 | /// The value to set in the cache. 61 | /// The cache options for the value. 62 | /// Optional. The used to propagate notifications that the operation should be canceled. 63 | public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions? options, 64 | CancellationToken token = new CancellationToken()) 65 | { 66 | if (value == null) throw new ArgumentNullException(nameof(value)); 67 | return _stringCache.SetAsync(key, Encoding.UTF8.GetString(value), options, token); 68 | } 69 | 70 | /// 71 | /// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any). 72 | /// 73 | /// A string identifying the requested value. 74 | public void Refresh(string key) 75 | { 76 | throw new NotImplementedException("This library doesn't support sliding expirations for performance reasons."); 77 | } 78 | 79 | /// 80 | /// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any). 81 | /// 82 | /// A string identifying the requested value. 83 | /// Optional. The used to propagate notifications that the operation should be canceled. 84 | public Task RefreshAsync(string key, CancellationToken token = new CancellationToken()) 85 | { 86 | throw new NotImplementedException("This library doesn't support sliding expirations for performance reasons."); 87 | } 88 | 89 | /// Removes the value with the given key. 90 | /// A string identifying the requested value. 91 | public void Remove(string key) 92 | { 93 | _stringCache.Remove(key); 94 | } 95 | 96 | /// Removes the value with the given key. 97 | /// A string identifying the requested value. 98 | /// Optional. The used to propagate notifications that the operation should be canceled. 99 | public Task RemoveAsync(string key, CancellationToken token = new CancellationToken()) 100 | { 101 | return _stringCache.RemoveAsync(key, token); 102 | } 103 | 104 | /// 105 | /// This clears all the key/value pairs from the json cache file 106 | /// 107 | public void ClearAll() 108 | { 109 | _stringCache.ClearAll(); 110 | } 111 | 112 | /// 113 | /// This return all the cached values as a dictionary 114 | /// 115 | /// 116 | public Dictionary GetAllKeyValues() 117 | { 118 | var stringValues = _stringCache.GetAllKeyValues(); 119 | 120 | var stringByteDictionary = new Dictionary(); 121 | foreach (var key in stringValues.Keys) 122 | { 123 | stringByteDictionary.Add(key, Encoding.UTF8.GetBytes(stringValues[key])); 124 | } 125 | 126 | return stringByteDictionary; 127 | } 128 | 129 | /// 130 | /// This return all the cached values as a dictionary, async 131 | /// 132 | /// 133 | public async Task> GetAllKeyValuesAsync() 134 | { 135 | var stringValues = await _stringCache.GetAllKeyValuesAsync(); 136 | 137 | var stringByteDictionary = new Dictionary(); 138 | foreach (var key in stringValues.Keys) 139 | { 140 | stringByteDictionary.Add(key, Encoding.UTF8.GetBytes(stringValues[key])); 141 | } 142 | 143 | return stringByteDictionary; 144 | } 145 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/DistributedFileStoreCacheClass.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text.Json; 5 | using Microsoft.Extensions.Caching.Distributed; 6 | 7 | namespace Net.DistributedFileStoreCache; 8 | 9 | /// 10 | /// This adds methods to serialize / deserialize classes into a string which is saved as a json string 11 | /// 12 | public class DistributedFileStoreCacheClass : DistributedFileStoreCacheString, IDistributedFileStoreCacheClass 13 | { 14 | private readonly DistributedFileStoreCacheOptions _options; 15 | 16 | /// 17 | /// ctor 18 | /// 19 | /// 20 | public DistributedFileStoreCacheClass(DistributedFileStoreCacheOptions options) 21 | : base(options) 22 | { 23 | _options = options; 24 | } 25 | 26 | /// 27 | /// This method is useful if you want to decode a cache value via the 28 | /// or the methods 29 | /// 30 | /// A class which can be created 31 | /// 32 | /// The deserialize class or null. 33 | public T? GetClassFromString(string? jsonString) where T : class, new() 34 | { 35 | return jsonString == null ? null : JsonSerializer.Deserialize(jsonString); 36 | } 37 | 38 | 39 | /// Gets a class stored as json linked to the given key. 40 | /// A string identifying the requested stored class. 41 | /// A class which can be created 42 | /// The deserialize class or null. 43 | public T? GetClass(string key) where T : class, new() 44 | { 45 | var stringValue = CacheFileHandler.GetValue(key); 46 | return stringValue == null ? null : JsonSerializer.Deserialize(stringValue); 47 | } 48 | 49 | /// Gets a class stored as json linked to the given key. 50 | /// A string identifying the requested stored class. 51 | /// Optional. The used to propagate notifications that the operation should be canceled. 52 | /// A class which can be created 53 | /// The located class or null withing a Task result. 54 | public async Task GetClassAsync(string key, CancellationToken token = new CancellationToken()) where T : class, new() 55 | { 56 | var stringValue = await CacheFileHandler.GetValueAsync(key, token); 57 | return stringValue == null ? null : JsonSerializer.Deserialize(stringValue); 58 | } 59 | 60 | /// Serializers the class and stores the json against the given key. 61 | /// A string identifying the requested value. 62 | /// The class that you wanted to be stored in the cache. 63 | /// The cache options for the value. 64 | /// A class which can be created 65 | public void SetClass(string key, T yourClass, DistributedCacheEntryOptions? options) where T : class, new() 66 | { 67 | var jsonString = JsonSerializer.Serialize(yourClass, _options.JsonSerializerForCacheFile); 68 | CacheFileHandler.SetKeyValue(key, jsonString, options); 69 | } 70 | 71 | /// Serializers the class and stores the json against the given key. 72 | /// A string identifying the requested value. 73 | /// The class that you wanted to be stored in the cache. 74 | /// The cache options for the value. 75 | /// Optional. The used to propagate notifications that the operation should be canceled. 76 | /// A class which can be created 77 | public Task SetClassAsync(string key, T yourClass, DistributedCacheEntryOptions? options, 78 | CancellationToken token = new ()) where T : class, new() 79 | { 80 | var jsonString = JsonSerializer.Serialize(yourClass, _options.JsonSerializerForCacheFile); 81 | return CacheFileHandler.SetKeyValueAsync(key, jsonString, options, token); 82 | } 83 | 84 | /// Serializes all the values in each KeyValue using the T type and save each into the cache 85 | /// List of KeyValuePairs to be added to the cache, with the values being serialized. 86 | /// Optional: The cache options for the value. 87 | /// A class which contains the data to stored as JSON in the cache 88 | public void SetManyClass(List> manyEntries, DistributedCacheEntryOptions? options) 89 | where T : class, new() 90 | { 91 | var keysWithJsonValues = manyEntries 92 | .Select(m => new KeyValuePair( 93 | m.Key, JsonSerializer.Serialize(m.Value, _options.JsonSerializerForCacheFile))) 94 | .ToList(); 95 | CacheFileHandler.SetKeyValueMany(keysWithJsonValues, options); 96 | } 97 | 98 | /// Serializes all the values in each KeyValue using the T type and save each into the cache 99 | /// List of KeyValuePairs to be added to the cache, with the values being serialized. 100 | /// Optional: The cache options for the value. 101 | /// Optional. The used to propagate notifications that the operation should be canceled. 102 | /// A class which contains the data to stored as JSON in the cache 103 | public Task SetManyClassAsync(List> manyEntries, DistributedCacheEntryOptions? options, 104 | CancellationToken token = new()) where T : class, new() 105 | { 106 | var keysWithJsonValues = manyEntries 107 | .Select(m => new KeyValuePair( 108 | m.Key, JsonSerializer.Serialize(m.Value, _options.JsonSerializerForCacheFile))) 109 | .ToList(); 110 | return CacheFileHandler.SetKeyValueManyAsync(keysWithJsonValues, options, token); 111 | } 112 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/DistributedFileStoreCacheNuGetIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JonPSmith/Net.DistributedFileStoreCache/2e51ab5ffc81c4ab012e7d820b998fe826852f2c/Net.DistributedFileStoreCache/DistributedFileStoreCacheNuGetIcon.png -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/DistributedFileStoreCacheOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text.Json; 5 | using Microsoft.Extensions.Caching.Distributed; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace Net.DistributedFileStoreCache; 9 | 10 | /// 11 | /// This provides the members to select which version of the FileStore cache class / interface you want registers as a service 12 | /// 13 | public enum FileStoreCacheVersions 14 | { 15 | /// 16 | /// Use this to register the against the interface 17 | /// 18 | String, 19 | /// 20 | /// Use this to register the against the interface 21 | /// 22 | Class, 23 | /// 24 | /// Use this to register the against the interface 25 | /// 26 | Bytes, 27 | /// 28 | /// Use this to register the against the interface 29 | /// 30 | // ReSharper disable once InconsistentNaming 31 | IDistributedCache 32 | } 33 | 34 | /// 35 | /// This contains all the options used to register / setup the FileStore cache 36 | /// 37 | public class DistributedFileStoreCacheOptions 38 | { 39 | /// 40 | /// This defines which version of the services are registered 41 | /// 1. Default is , where the value is of type string, plus two extra features 42 | /// 2. If set to this handles classes and type string, plus two extra features 43 | /// 3. If set to , where the value is of type byte[], plus two extra features 44 | /// 4. If set to , which implements the interface 45 | /// 46 | public FileStoreCacheVersions WhichVersion { get; set; } 47 | 48 | /// 49 | /// This defines the maximum bytes that can be in the cache json file. 50 | /// If you exceed this, then you will have an exception, so I recommend you use the 51 | /// to get the correct size. 52 | /// See https://github.com/JonPSmith/Net.DistributedFileStoreCache/wiki/Tips-on-making-your-cache-fast 53 | /// 54 | public int MaxBytesInJsonCacheFile { get; set; } = 10_000; 55 | 56 | /// 57 | /// If you want to set the maximum bytes that the cache can hold then you can use this calculation 58 | /// 59 | /// The maximum number of cache entries you want to add to the cache 60 | /// The maximum size of the key string. ASSUMES ASCII characters. 61 | /// Use the parameter to define what type of data you are caching. 62 | /// The maximum size of the value string 63 | /// Optional: 64 | /// If ascii data = 1, if unicode and not using UnsafeRelaxedJsonEscaping then 6, 65 | /// if unicode with UnsafeRelaxedJsonEscaping then 2, 66 | /// if not UTF8 character the 6 67 | /// Optional: the percent (between 0 and 100) of the entries will have a timeout 68 | public void SetMaxBytesByCalculation(int maxEntries, int maxKeyLength, int maxValueLength, int charSize = 1, int percentWithTimeout = 0) 69 | { 70 | maxValueLength *= charSize ; 71 | MaxBytesInJsonCacheFile = maxEntries * (maxKeyLength + maxValueLength + 6) + 15 + 14; 72 | if (percentWithTimeout > 0) 73 | MaxBytesInJsonCacheFile += (int)(maxEntries * percentWithTimeout / 100.0 * 74 | (maxKeyLength + DateTime.MaxValue.Ticks.ToString().Length + 6)); 75 | } 76 | 77 | /// 78 | /// This allows you to replace the System.Text.Json default serialization options. Here are some reasons you might want to so this: 79 | /// 1. If you are using Unicode characters in the version then set this parameter to 80 | /// containing { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }, 81 | /// to make the Unicode smaller (and easier to read). 82 | /// 2. The default serialization creates one long line, which is efficient on space but hard to read. 83 | /// If you set this parameter to a containing { WriteIndented = true }, 84 | /// then it takes up LOT MORE SPACE so only use it to debug a problem but its easier to read. 85 | /// NOTE: It you don's set this parameter will added, with the UnsafeRelaxedJsonEscaping if the 86 | /// version is selected. 87 | /// 88 | public JsonSerializerOptions? JsonSerializerForCacheFile { get; set; } 89 | 90 | /// 91 | /// This provides the path to the directory containing the cache file name 92 | /// If null, this will be set to the .. 93 | /// But you can set your own filepath by setting this parameter 94 | /// 95 | public string? PathToCacheFileDirectory { get; set; } 96 | 97 | /// 98 | /// This provides a suffix to the cache file name 99 | /// - useful to stop development file effect the production 100 | /// If null, this will be set to the .. 101 | /// But you can replace the name from the environment settings 102 | /// 103 | public string? SecondPartOfCacheFileName { get; set; } 104 | 105 | /// 106 | /// This holds the first part of the distributed cache file used by the . 107 | /// Note that it shouldn't have the file type (e.g. ".json") on the name 108 | /// 109 | public string FirstPartOfCacheFileName { get; set; } = "FileStoreCacheFile"; 110 | 111 | /// 112 | /// This sets the delay between a retry after a is throw 113 | /// NOTE: Keep it small 114 | /// 115 | public int DelayMillisecondsOnUnauthorizedAccess { get; set; } = 5; 116 | 117 | /// 118 | /// This sets the number of retries after a is throw 119 | /// 120 | public int NumTriesOnUnauthorizedAccess { get; set; } = 20; 121 | 122 | /// 123 | /// By default this will check that you are trying to register more then one . 124 | /// You need to set this to true if you are running unit tests with different cache file names (and run tests serially) 125 | /// 126 | public bool TurnOffStaticFilePathCheck { get; set; } = false; 127 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/DistributedFileStoreCacheString.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.Caching.Distributed; 5 | using Net.DistributedFileStoreCache.SupportCode; 6 | 7 | namespace Net.DistributedFileStoreCache; 8 | 9 | /// 10 | /// This is the Distributed FileStore cache that has a value of type string. 11 | /// This is the primary FileStore cache version that the other versions link to this class 12 | /// 13 | public class DistributedFileStoreCacheString : IDistributedFileStoreCacheString 14 | { 15 | /// 16 | /// This class directly creates the which provides read/write access of the cache json file 17 | /// 18 | protected readonly CacheFileHandler CacheFileHandler; 19 | 20 | /// 21 | /// ctor 22 | /// 23 | /// 24 | public DistributedFileStoreCacheString(DistributedFileStoreCacheOptions fileStoreCacheOptions) 25 | { 26 | CacheFileHandler = new CacheFileHandler(fileStoreCacheOptions); 27 | } 28 | 29 | /// Gets a value with the given key. 30 | /// A string identifying the requested value. 31 | /// The located value or null. 32 | public string? Get(string key) 33 | { 34 | return CacheFileHandler.GetValue(key); 35 | } 36 | 37 | /// Gets a value with the given key. 38 | /// A string identifying the requested value. 39 | /// Optional. The used to propagate notifications that the operation should be canceled. 40 | /// The that represents the asynchronous operation, containing the located value or null. 41 | public Task GetAsync(string key, CancellationToken token = new CancellationToken()) 42 | { 43 | return CacheFileHandler.GetValueAsync(key, token); 44 | } 45 | 46 | /// Sets a value with the given key. 47 | /// A string identifying the requested value. 48 | /// The value to set in the cache. 49 | /// The cache options for the value. 50 | public void Set(string key, string value, DistributedCacheEntryOptions? options) 51 | { 52 | CacheFileHandler.SetKeyValue(key, value, options); 53 | } 54 | 55 | /// Sets the value with the given key. 56 | /// A string identifying the requested value. 57 | /// The value to set in the cache. 58 | /// The cache options for the value. 59 | /// Optional. The used to propagate notifications that the operation should be canceled. 60 | /// The that represents the asynchronous operation. 61 | public Task SetAsync(string key, string value, DistributedCacheEntryOptions? options, 62 | CancellationToken token = new CancellationToken()) 63 | { 64 | return CacheFileHandler.SetKeyValueAsync(key, value, options, token); 65 | } 66 | 67 | /// Sets many entries via a list of KeyValues 68 | /// List of KeyValuePairs to be added to the cache. 69 | /// Optional: The cache options for the value. 70 | public void SetMany(List> manyEntries, DistributedCacheEntryOptions? options) 71 | { 72 | CacheFileHandler.SetKeyValueMany(manyEntries, options); 73 | } 74 | 75 | 76 | /// Sets many entries via a list of KeyValues 77 | /// List of KeyValuePairs to be added to the cache. 78 | /// Optional: The cache options for the value. 79 | /// Optional. The used to propagate notifications that the operation should be canceled. 80 | public Task SetManyAsync(List> manyEntries, DistributedCacheEntryOptions? options, 81 | CancellationToken token = new ()) 82 | { 83 | return CacheFileHandler.SetKeyValueManyAsync(manyEntries, options, token); 84 | } 85 | 86 | /// Removes the value with the given key. 87 | /// A string identifying the requested value. 88 | public void Remove(string key) 89 | { 90 | CacheFileHandler.RemoveKeyValue(key); 91 | } 92 | 93 | /// Removes the value with the given key. 94 | /// A string identifying the requested value. 95 | /// Optional. The used to propagate notifications that the operation should be canceled. 96 | /// The that represents the asynchronous operation. 97 | public Task RemoveAsync(string key, CancellationToken token = new CancellationToken()) 98 | { 99 | return CacheFileHandler.RemoveKeyValueAsync(key, token); 100 | } 101 | 102 | /// 103 | /// This clears all the key/value pairs from the json cache file, with option to add entries after the cache is cleared. 104 | /// 105 | /// Optional: After of the clearing the cache these KeyValues will written into the cache 106 | /// Optional: If there are entries to add to the cache, this will set the timeout time. 107 | public void ClearAll(List>? manyEntries = null, DistributedCacheEntryOptions? entryOptions = null) 108 | { 109 | CacheFileHandler.ResetCacheFile(manyEntries, entryOptions); 110 | } 111 | 112 | /// 113 | /// This return all the cached values as a dictionary 114 | /// 115 | /// 116 | public IReadOnlyDictionary GetAllKeyValues() 117 | { 118 | return CacheFileHandler.GetAllValues(); 119 | } 120 | 121 | /// 122 | /// This return all the cached values as a dictionary 123 | /// 124 | /// 125 | public Task> GetAllKeyValuesAsync(CancellationToken token = new CancellationToken()) 126 | { 127 | return CacheFileHandler.GetAllValuesAsync(token); 128 | } 129 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/IDistributedFileStoreCacheBytes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.Caching.Distributed; 5 | 6 | namespace Net.DistributedFileStoreCache; 7 | 8 | /// 9 | /// This adds a couple of useful features beyond the interface 10 | /// 11 | public interface IDistributedFileStoreCacheBytes : IDistributedCache 12 | { 13 | /// 14 | /// This clears all the key/value pairs from the json cache file 15 | /// 16 | void ClearAll(); 17 | 18 | /// 19 | /// This return all the cached values as a dictionary 20 | /// 21 | /// 22 | Dictionary GetAllKeyValues(); 23 | 24 | /// 25 | /// This return all the cached values as a dictionary 26 | /// 27 | /// 28 | Task> GetAllKeyValuesAsync(); 29 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/IDistributedFileStoreCacheClass.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.Caching.Distributed; 5 | using Net.DistributedFileStoreCache.SupportCode; 6 | 7 | namespace Net.DistributedFileStoreCache; 8 | 9 | /// 10 | /// Interface for the Class FileStore cache version. Note that it also inherits the interface. 11 | /// 12 | public interface IDistributedFileStoreCacheClass : IDistributedFileStoreCacheString 13 | { 14 | /// 15 | /// This method is useful if you want to decode a cache value via the 16 | /// or the methods 17 | /// 18 | /// A class which can be created 19 | /// 20 | /// The deserialize class or null. 21 | T? GetClassFromString(string? jsonString) where T : class, new(); 22 | 23 | /// Gets a class stored as json linked to the given key. 24 | /// A string identifying the requested stored class. 25 | /// A class which can be created 26 | /// The deserialize class or null. 27 | T? GetClass(string key) where T : class, new(); 28 | 29 | /// Gets a class stored as json linked to the given key. 30 | /// A string identifying the requested stored class. 31 | /// Optional. The used to propagate notifications that the operation should be canceled. 32 | /// A class which can be created 33 | /// The located class or null withing a Task result. 34 | Task GetClassAsync(string key, CancellationToken token = new CancellationToken()) where T : class, new(); 35 | 36 | /// Serializers the class and stores the json against the given key. 37 | /// A string identifying the requested value. 38 | /// The class that you wanted to be stored in the cache. 39 | /// The cache options for the value. 40 | /// A class which contains the data to stored as JSON in the cache 41 | void SetClass(string key, T yourClass, DistributedCacheEntryOptions? options = null) where T : class, new(); 42 | 43 | /// Serializers the class and stores the json against the given key. 44 | /// A string identifying the requested value. 45 | /// The class that you wanted to be stored in the cache. 46 | /// The cache options for the value. 47 | /// Optional. The used to propagate notifications that the operation should be canceled. 48 | /// A class which contains the data to stored as JSON in the cache 49 | Task SetClassAsync(string key, T yourClass, DistributedCacheEntryOptions? options = null, 50 | CancellationToken token = new CancellationToken()) where T : class, new(); 51 | 52 | /// Serializes all the values in each KeyValue using the T type and save each into the cache 53 | /// List of KeyValuePairs to be added to the cache, with the values being serialized. 54 | /// Optional: The cache options for the value. 55 | /// A class which contains the data to stored as JSON in the cache 56 | void SetManyClass(List> manyEntries, DistributedCacheEntryOptions? options = null) 57 | where T : class, new(); 58 | 59 | /// Serializes all the values in each KeyValue using the T type and save each into the cache 60 | /// List of KeyValuePairs to be added to the cache, with the values being serialized. 61 | /// Optional: The cache options for the value. 62 | /// Optional. The used to propagate notifications that the operation should be canceled. 63 | /// A class which contains the data to stored as JSON in the cache 64 | Task SetManyClassAsync(List> manyEntries, DistributedCacheEntryOptions? options = null, 65 | CancellationToken token = new()) where T : class, new(); 66 | 67 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/IDistributedFileStoreCacheString.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.Caching.Distributed; 5 | 6 | namespace Net.DistributedFileStoreCache; 7 | 8 | /// 9 | /// Interface for the String FileStore cache version 10 | /// 11 | public interface IDistributedFileStoreCacheString 12 | { 13 | /// Gets a value with the given key. 14 | /// A string identifying the requested value. 15 | /// The located value or null. 16 | string? Get(string key); 17 | 18 | /// Gets a value with the given key. 19 | /// A string identifying the requested value. 20 | /// Optional. The used to propagate notifications that the operation should be canceled. 21 | /// The that represents the asynchronous operation, containing the located value or null. 22 | Task GetAsync(string key, CancellationToken token = new ()); 23 | 24 | /// Sets a value with the given key. 25 | /// A string identifying the requested value. 26 | /// The value to set in the cache. 27 | /// The cache options for the value. 28 | void Set(string key, string value, DistributedCacheEntryOptions? options = null); 29 | 30 | /// Sets the value with the given key. 31 | /// A string identifying the requested value. 32 | /// The value to set in the cache. 33 | /// The cache options for the value. 34 | /// Optional. The used to propagate notifications that the operation should be canceled. 35 | /// The that represents the asynchronous operation. 36 | Task SetAsync(string key, string value, DistributedCacheEntryOptions? options = null, 37 | CancellationToken token = new ()); 38 | 39 | /// Sets many entries via a list of KeyValues 40 | /// List of KeyValuePairs to be added to the cache. 41 | /// Optional: The cache options for the value. 42 | void SetMany(List> manyEntries, DistributedCacheEntryOptions? options = null); 43 | 44 | /// Sets many entries via a list of KeyValues 45 | /// List of KeyValuePairs to be added to the cache. 46 | /// Optional: The cache options for the value. 47 | /// Optional. The used to propagate notifications that the operation should be canceled. 48 | Task SetManyAsync(List> manyEntries, DistributedCacheEntryOptions? options = null, 49 | CancellationToken token = new()); 50 | 51 | /// Removes the value with the given key. 52 | /// A string identifying the requested value. 53 | void Remove(string key); 54 | 55 | /// Removes the value with the given key. 56 | /// A string identifying the requested value. 57 | /// Optional. The used to propagate notifications that the operation should be canceled. 58 | /// The that represents the asynchronous operation. 59 | Task RemoveAsync(string key, CancellationToken token = new CancellationToken()); 60 | 61 | /// 62 | /// This clears all the key/value pairs from the json cache file, with option to add entries after the cache is cleared. 63 | /// 64 | /// Optional: After of the clearing the cache these KeyValues will written into the cache 65 | /// Optional: If there are entries to add to the cache, this will set the timeout time. 66 | void ClearAll(List>? manyEntries = null, 67 | DistributedCacheEntryOptions? entryOptions = null); 68 | 69 | /// 70 | /// This return all the cached values as a dictionary 71 | /// 72 | /// 73 | IReadOnlyDictionary GetAllKeyValues(); 74 | 75 | /// 76 | /// This return all the cached values as a dictionary 77 | /// 78 | /// 79 | Task> GetAllKeyValuesAsync(CancellationToken token = new CancellationToken()); 80 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/Net.DistributedFileStoreCache.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | latest 6 | enable 7 | enable 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Net.DistributedFileStoreCache 22 | 2.0.0 23 | Jon P Smith 24 | A fast .NET distributed cache using a json file as the shared resource 25 | false 26 | 27 | Changed target framework to netstandard2.1 to work with any version of .NET 28 | 29 | Copyright (c) 2022 Jon P Smith. Licenced under MIT licence 30 | Distributed cache 31 | true 32 | true 33 | https://github.com/JonPSmith/Net.DistributedFileStoreCache 34 | https://github.com/JonPSmith/Net.DistributedFileStoreCache 35 | DistributedFileStoreCacheNuGetIcon.png 36 | MIT 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/RegisterDistributedFileStoreCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text.Encodings.Web; 5 | using System.Text.Json; 6 | using Microsoft.Extensions.Caching.Distributed; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Net.DistributedFileStoreCache.SupportCode; 10 | 11 | namespace Net.DistributedFileStoreCache; 12 | 13 | /// 14 | /// This class contains the register / setup of the distributed FileStore cache 15 | /// 16 | public static class RegisterDistributedFileStoreCache 17 | { 18 | /// 19 | /// Use this to register the version of the distributed FileStore cache service you want. 20 | /// It also ensures the cache json file is setup 21 | /// 22 | /// Needs the to register the selected distributed FileStore cache version 23 | /// This allows to set up any of the properties in the class 24 | /// Optional: If provided it sets the property 25 | /// from the and the property 26 | /// from the property. 27 | /// 28 | /// , which is useful in unit testing. 29 | /// 30 | /// 31 | /// 32 | public static DistributedFileStoreCacheOptions AddDistributedFileStoreCache(this IServiceCollection services, 33 | Action? optionsAction = null, 34 | IHostEnvironment? environment = null) 35 | { 36 | if (services == null) throw new ArgumentNullException(nameof(services)); 37 | 38 | var options = new DistributedFileStoreCacheOptions(); 39 | optionsAction?.Invoke(options); 40 | options.PathToCacheFileDirectory ??= environment?.ContentRootPath; 41 | options.SecondPartOfCacheFileName ??= environment?.EnvironmentName; 42 | 43 | options.JsonSerializerForCacheFile ??= options.WhichVersion == FileStoreCacheVersions.Class 44 | // if the JsonSerializerForCacheFile isn't already set up and the version is Class, then add UnsafeRelaxedJsonEscaping 45 | ? new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping } 46 | : new JsonSerializerOptions(); 47 | 48 | if (options.PathToCacheFileDirectory == null) 49 | throw new DistributedFileStoreCacheException( 50 | $"You either need to provide a value for the {nameof(environment)} parameter, " + 51 | $"or set the options' {nameof(DistributedFileStoreCacheOptions.PathToCacheFileDirectory)} property."); 52 | 53 | if (options.SecondPartOfCacheFileName == null) 54 | throw new DistributedFileStoreCacheException( 55 | $"You either need to provide a value for the {nameof(environment)} parameter, " + 56 | $"or set the options' {nameof(DistributedFileStoreCacheOptions.SecondPartOfCacheFileName)} property."); 57 | 58 | //Set up the static file watcher 59 | StaticCachePart.SetupStaticCache(options); 60 | 61 | // Add services to the container. 62 | switch (options.WhichVersion) 63 | { 64 | case FileStoreCacheVersions.String: 65 | services.AddSingleton(new DistributedFileStoreCacheString(options)); 66 | break; 67 | case FileStoreCacheVersions.Class: 68 | services.AddSingleton(new DistributedFileStoreCacheClass(options)); 69 | break; 70 | case FileStoreCacheVersions.Bytes: 71 | services.AddSingleton(new DistributedFileStoreCacheBytes(new DistributedFileStoreCacheString(options))); 72 | break; 73 | case FileStoreCacheVersions.IDistributedCache: 74 | services.AddSingleton(new DistributedFileStoreCacheBytes(new DistributedFileStoreCacheString(options)) as IDistributedCache); 75 | break; 76 | default: 77 | throw new ArgumentOutOfRangeException(); 78 | } 79 | 80 | return options; 81 | } 82 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/CacheFileExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace Net.DistributedFileStoreCache.SupportCode; 5 | 6 | 7 | /// 8 | /// This class contains extension methods using the 9 | /// 10 | public static class CacheFileExtensions 11 | { 12 | /// 13 | /// This returns the cache json filename, including its type 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static string FormCacheFileName(this DistributedFileStoreCacheOptions fileStoreCacheOptions) 19 | { 20 | if (fileStoreCacheOptions == null) throw new ArgumentNullException(nameof(fileStoreCacheOptions)); 21 | return $"{fileStoreCacheOptions.FirstPartOfCacheFileName}.{fileStoreCacheOptions.SecondPartOfCacheFileName}.json"; 22 | } 23 | 24 | /// 25 | /// This returns the FilePath to the cache json file 26 | /// 27 | /// 28 | /// 29 | /// 30 | public static string FormCacheFilePath(this DistributedFileStoreCacheOptions fileStoreCacheOptions) 31 | { 32 | if (fileStoreCacheOptions == null) throw new ArgumentNullException(nameof(fileStoreCacheOptions)); 33 | if (fileStoreCacheOptions.PathToCacheFileDirectory == null) 34 | throw new ArgumentNullException(nameof(fileStoreCacheOptions.PathToCacheFileDirectory)); 35 | return Path.Combine(fileStoreCacheOptions.PathToCacheFileDirectory, fileStoreCacheOptions.FormCacheFileName()); 36 | } 37 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/CacheFileHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text; 5 | using System.Text.Json; 6 | using Microsoft.Extensions.Caching.Distributed; 7 | 8 | namespace Net.DistributedFileStoreCache.SupportCode; 9 | 10 | /// 11 | /// This class contains all the code that accesses the local static cache and the json cache file. 12 | /// This class should be internal, but you can't use protected in a public class. 13 | /// 14 | public class CacheFileHandler 15 | { 16 | private readonly DistributedFileStoreCacheOptions _options; 17 | 18 | /// 19 | /// ctor 20 | /// 21 | /// 22 | public CacheFileHandler (DistributedFileStoreCacheOptions options) 23 | { 24 | _options = options; 25 | } 26 | 27 | /// 28 | /// This handles the Get 29 | /// 30 | /// 31 | /// 32 | public string? GetValue(string key) 33 | { 34 | if (StaticCachePart.LocalCacheIsOutOfDate) 35 | _options.TryAgainOnUnauthorizedAccess(UpdateLocalCacheFromCacheFile); 36 | 37 | return StaticCachePart.CacheContent.ReturnNullIfExpires(key); 38 | } 39 | 40 | /// 41 | /// This handles the GetAsync 42 | /// 43 | /// 44 | /// 45 | /// 46 | public async Task GetValueAsync(string key, CancellationToken token) 47 | { 48 | if (StaticCachePart.LocalCacheIsOutOfDate) 49 | await _options.TryAgainOnUnauthorizedAccessAsync(() => UpdateLocalCacheFromCacheFileAsync(token)); 50 | 51 | return StaticCachePart.CacheContent.ReturnNullIfExpires(key); 52 | } 53 | 54 | /// 55 | /// This handles the GetAllValues 56 | /// 57 | /// 58 | public IReadOnlyDictionary GetAllValues() 59 | { 60 | if (StaticCachePart.LocalCacheIsOutOfDate) 61 | _options.TryAgainOnUnauthorizedAccess(UpdateLocalCacheFromCacheFile); 62 | 63 | return StaticCachePart.CacheContent.ReturnNonExpiredCacheValues(); 64 | } 65 | 66 | /// 67 | /// This handles the GetAllValuesAsync 68 | /// 69 | /// 70 | /// 71 | public async Task> GetAllValuesAsync(CancellationToken token) 72 | { 73 | if (StaticCachePart.LocalCacheIsOutOfDate) 74 | await _options.TryAgainOnUnauthorizedAccessAsync(() => UpdateLocalCacheFromCacheFileAsync(token)); 75 | 76 | return StaticCachePart.CacheContent.ReturnNonExpiredCacheValues(); 77 | } 78 | 79 | /// 80 | /// This handles the Set 81 | /// 82 | /// 83 | /// 84 | /// 85 | public void SetKeyValue(string key, string value, DistributedCacheEntryOptions? entryOptions) 86 | { 87 | var setter = new CacheFileSetOne(key, value, entryOptions); 88 | _options.TryAgainOnUnauthorizedAccess(() => 89 | ReadAndChangeCacheJsonFile(setter.SetKeyValueHandler, false) 90 | .CheckSyncValueTaskWorked()); 91 | } 92 | 93 | /// 94 | /// This handles the SetAsync 95 | /// 96 | /// 97 | /// 98 | /// 99 | /// 100 | /// 101 | public async Task SetKeyValueAsync(string key, string value, DistributedCacheEntryOptions? entryOptions, 102 | CancellationToken token) 103 | { 104 | var setter = new CacheFileSetOne(key, value, entryOptions); 105 | await _options.TryAgainOnUnauthorizedAccessAsync(async () => 106 | await ReadAndChangeCacheJsonFile(setter.SetKeyValueHandler, true, token: token)); 107 | } 108 | 109 | /// 110 | /// This handles the SetMany 111 | /// 112 | /// 113 | /// 114 | public void SetKeyValueMany(List> manyEntries, DistributedCacheEntryOptions? entryOptions) 115 | { 116 | var setMany = new CacheFileSetMany(manyEntries, entryOptions); 117 | _options.TryAgainOnUnauthorizedAccess(() => 118 | ReadAndChangeCacheJsonFile(setMany.SetManyKeyValueHandler, false) 119 | .CheckSyncValueTaskWorked()); 120 | } 121 | 122 | /// 123 | /// This handles the SetManyAsync 124 | /// 125 | /// 126 | /// 127 | /// 128 | /// 129 | public async Task SetKeyValueManyAsync(List> manyEntries, DistributedCacheEntryOptions? entryOptions, 130 | CancellationToken token) 131 | { 132 | var setMany = new CacheFileSetMany(manyEntries, entryOptions); 133 | await _options.TryAgainOnUnauthorizedAccessAsync(async () => 134 | await ReadAndChangeCacheJsonFile(setMany.SetManyKeyValueHandler, true, token: token)); 135 | } 136 | 137 | /// 138 | /// This handles the Remove 139 | /// 140 | /// 141 | public void RemoveKeyValue(string key) 142 | { 143 | var remover = new CacheFileRemove(key); 144 | _options.TryAgainOnUnauthorizedAccess(() => 145 | ReadAndChangeCacheJsonFile(remover.RemoveKeyValueHandler, false) 146 | .CheckSyncValueTaskWorked()); 147 | } 148 | 149 | 150 | /// 151 | /// This handles the RemoveAsync 152 | /// 153 | /// 154 | /// 155 | /// 156 | public async Task RemoveKeyValueAsync(string key, CancellationToken token) 157 | { 158 | var remover = new CacheFileRemove(key); 159 | await _options.TryAgainOnUnauthorizedAccessAsync(async () => 160 | await ReadAndChangeCacheJsonFile(remover.RemoveKeyValueHandler, false, token: token)); 161 | } 162 | 163 | /// 164 | /// This handles the ClearAll 165 | /// 166 | /// if not null, then after of the clearing the cache these KeyValues will written into the cache 167 | /// Optional: If there are entries to add to the cache, this will set the timeout time. 168 | public void ResetCacheFile(List>? manyEntries, DistributedCacheEntryOptions? entryOptions) 169 | { 170 | var setMany = new CacheFileSetMany(manyEntries, entryOptions); 171 | _options.TryAgainOnUnauthorizedAccess(() => 172 | ReadAndChangeCacheJsonFile(setMany.SetManyKeyValueHandler , false, true).CheckSyncValueTaskWorked()); 173 | } 174 | 175 | 176 | /// 177 | /// This should ONLY be used on startup. Its job is to ensure there is a cache file 178 | /// 179 | public void CreateNewCacheFileIfMissingWithRetry() 180 | { 181 | //Create a valid cache file containing no key/values 182 | var writeBytes = FillByteBufferWithCacheJsonData(new CacheJsonContent()); 183 | 184 | //We run this within a retry loop to make sure it succeeds 185 | _options.TryAgainOnUnauthorizedAccess(() => 186 | { 187 | var cacheFilePath = _options.FormCacheFilePath(); 188 | if (!File.Exists(cacheFilePath)) 189 | { 190 | //This uses FileMode.CreateNew to ensure only one file is created 191 | using FileStream writeStream = new FileStream(cacheFilePath, FileMode.CreateNew, FileAccess.Write, 192 | FileShare.None, bufferSize: 1, false); 193 | { 194 | writeStream.Write(writeBytes, 0, writeBytes.Length); 195 | } 196 | } 197 | }); 198 | } 199 | 200 | //----------------------------------------------------------------- 201 | //private methods 202 | 203 | private void UpdateLocalCacheFromCacheFile() 204 | { 205 | var readBuffer = new byte[_options.MaxBytesInJsonCacheFile]; 206 | var readFilePath = _options.FormCacheFilePath(); 207 | 208 | //This uses FileShare.None to ensure multiple instances don't try to update the in-memory cache at the same time 209 | using FileStream readStream = new FileStream(readFilePath, FileMode.Open, FileAccess.Read, FileShare.None, 210 | bufferSize: 1, false); 211 | { 212 | var numBytesRead = readStream.Read(readBuffer); 213 | if (numBytesRead >= _options.MaxBytesInJsonCacheFile) 214 | throw new DistributedFileStoreCacheException( 215 | $"Your cache json file has more that {_options.MaxBytesInJsonCacheFile} " + 216 | $"bytes, so you MUST set the option's {nameof(DistributedFileStoreCacheOptions.MaxBytesInJsonCacheFile)} to a bigger value."); 217 | 218 | StaticCachePart.UpdateLocalCache(GetJsonFromByteBuffer(numBytesRead, ref readBuffer)); 219 | } 220 | } 221 | 222 | private async ValueTask UpdateLocalCacheFromCacheFileAsync(CancellationToken token) 223 | { 224 | var readBuffer = new byte[_options.MaxBytesInJsonCacheFile]; 225 | var readFilePath = _options.FormCacheFilePath(); 226 | //This uses FileShare.None to ensure multiple instances don't try to update the in-memory cache at the same time 227 | using FileStream readStream = new FileStream(readFilePath, FileMode.Open, FileAccess.Read, FileShare.None, 228 | bufferSize: 1, true); 229 | { 230 | var numBytesRead = await readStream.ReadAsync(readBuffer, token); 231 | if (numBytesRead >= _options.MaxBytesInJsonCacheFile) 232 | throw new DistributedFileStoreCacheException( 233 | $"Your cache json file has more that {_options.MaxBytesInJsonCacheFile} " + 234 | $"bytes, so you MUST set the option's {nameof(DistributedFileStoreCacheOptions.MaxBytesInJsonCacheFile)} to a bigger value."); 235 | 236 | StaticCachePart.UpdateLocalCache(GetJsonFromByteBuffer(numBytesRead, ref readBuffer)); 237 | } 238 | } 239 | 240 | /// 241 | /// delegate to use in methods 242 | /// 243 | /// 244 | public delegate void UpdateJsonDelegate(ref CacheJsonContent updateCurrentJson); 245 | 246 | private async ValueTask ReadAndChangeCacheJsonFile(UpdateJsonDelegate? updateCurrentJson, bool useAsync, 247 | bool reset = false, CancellationToken token = new ()) 248 | { 249 | //thanks to https://stackoverflow.com/questions/15628902/lock-file-exclusively-then-delete-move-it for this approach 250 | 251 | int numBytesRead = 0; 252 | var readWriteBuffer = new byte[_options.MaxBytesInJsonCacheFile]; 253 | var filePath = _options.FormCacheFilePath(); 254 | using FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None, bufferSize: 1, useAsync); 255 | { 256 | if(!reset) 257 | { 258 | numBytesRead = useAsync 259 | ? await fileStream.ReadAsync(readWriteBuffer, token) 260 | : fileStream.Read(readWriteBuffer); 261 | if (numBytesRead >= _options.MaxBytesInJsonCacheFile) 262 | throw new DistributedFileStoreCacheException( 263 | $"Your cache json file has more that {_options.MaxBytesInJsonCacheFile} " + 264 | $"bytes, so you MUST set the option's {nameof(DistributedFileStoreCacheOptions.MaxBytesInJsonCacheFile)} to a bigger value."); 265 | } 266 | 267 | var json = reset ? new CacheJsonContent() : GetJsonFromByteBuffer(numBytesRead, ref readWriteBuffer); 268 | updateCurrentJson?.Invoke(ref json); 269 | 270 | var bytesToWrite = FillByteBufferWithCacheJsonData(json); 271 | if (bytesToWrite.Length < _options.MaxBytesInJsonCacheFile) 272 | { 273 | //If the data has become longer that the set bytes, then we don't update the cache file (which means the change is lost) 274 | 275 | //thanks to https://stackoverflow.com/questions/15628902/lock-file-exclusively-then-delete-move-it 276 | fileStream.Seek(0, SeekOrigin.Begin); 277 | fileStream.SetLength(0); 278 | if (useAsync) 279 | await fileStream.WriteAsync(bytesToWrite, token); 280 | else 281 | fileStream.Write(bytesToWrite); 282 | 283 | //This is here to try and negate the first trigger of the file change 284 | StaticCachePart.UpdateLocalCache(json); 285 | } 286 | } 287 | } 288 | 289 | private CacheJsonContent GetJsonFromByteBuffer(int numBytes, ref byte[] buffer) 290 | { 291 | if (numBytes == 0) 292 | return new CacheJsonContent(); 293 | var jsonString = Encoding.UTF8.GetString(buffer, 0, numBytes); 294 | 295 | var cacheContent = JsonSerializer.Deserialize(jsonString)!; 296 | cacheContent.RemoveExpiredCacheValues(); 297 | return cacheContent; 298 | } 299 | 300 | private byte[] FillByteBufferWithCacheJsonData(CacheJsonContent allCache) 301 | { 302 | var jsonString = JsonSerializer.Serialize(allCache, _options.JsonSerializerForCacheFile); 303 | 304 | return Encoding.UTF8.GetBytes(jsonString); 305 | } 306 | 307 | 308 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/CacheFileRemove.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace Net.DistributedFileStoreCache.SupportCode; 5 | 6 | internal class CacheFileRemove 7 | { 8 | private readonly string _key; 9 | 10 | public CacheFileRemove(string key) 11 | { 12 | _key = key ?? throw new ArgumentNullException(nameof(key), "The key cannot be null"); 13 | } 14 | 15 | public void RemoveKeyValueHandler(ref CacheJsonContent currentJson) 16 | { 17 | currentJson.Cache.Remove(_key); 18 | currentJson.TimeOuts.Remove(_key); 19 | } 20 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/CacheFileSetMany.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.Caching.Distributed; 5 | 6 | namespace Net.DistributedFileStoreCache.SupportCode; 7 | 8 | internal class CacheFileSetMany 9 | { 10 | private readonly List>? _manyEntries; 11 | private readonly DistributedCacheEntryOptions? _timeoutOptions; 12 | 13 | public CacheFileSetMany(List>? manyEntries, DistributedCacheEntryOptions? timeoutOptions) 14 | { 15 | _manyEntries = manyEntries; 16 | _timeoutOptions = timeoutOptions; 17 | } 18 | 19 | public void SetManyKeyValueHandler(ref CacheJsonContent currentJson) 20 | { 21 | if (_manyEntries == null || !_manyEntries.Any()) 22 | return; 23 | 24 | foreach (var keyValue in _manyEntries) 25 | { 26 | if (keyValue.Key == null) throw new NullReferenceException("The key of a KeyPair cannot be null"); 27 | if (keyValue.Value == null) throw new NullReferenceException("The value of a KeyPair cannot be null"); 28 | currentJson.Cache[keyValue.Key] = keyValue.Value; 29 | ExpirationExtensions.SetupTimeoutIfOptions(ref currentJson, keyValue.Key, _timeoutOptions); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/CacheFileSetOne.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.Caching.Distributed; 5 | 6 | namespace Net.DistributedFileStoreCache.SupportCode; 7 | 8 | internal class CacheFileSetOne 9 | { 10 | private readonly string _key; 11 | private readonly string _value; 12 | private readonly DistributedCacheEntryOptions? _timeoutOptions; 13 | 14 | public CacheFileSetOne(string key, string value, DistributedCacheEntryOptions? timeoutOptions) 15 | { 16 | 17 | _key = key ?? throw new ArgumentNullException(nameof(key), "The key cannot be null"); 18 | _value = value ?? throw new ArgumentNullException(nameof(value), "The value cannot be null"); 19 | _timeoutOptions = timeoutOptions; 20 | } 21 | 22 | public void SetKeyValueHandler(ref CacheJsonContent currentJson) 23 | { 24 | currentJson.Cache[_key] = _value; 25 | ExpirationExtensions.SetupTimeoutIfOptions(ref currentJson, _key, _timeoutOptions); 26 | } 27 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/CacheJsonContent.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace Net.DistributedFileStoreCache.SupportCode; 5 | 6 | /// 7 | /// This class defines the content of the json cache file 8 | /// 9 | public class CacheJsonContent 10 | { 11 | /// 12 | /// This holds all the cache entries 13 | /// 14 | public Dictionary Cache { get; set; } = new Dictionary(); 15 | 16 | /// 17 | /// This contains all the absolute timeout applied to an cache entry. The cache entry key is used. 18 | /// 19 | public Dictionary TimeOuts { get; set; } = new Dictionary(); 20 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/DistributedFileStoreCacheException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace Net.DistributedFileStoreCache.SupportCode; 5 | 6 | /// 7 | /// This is a exception to use if a error occurs 8 | /// 9 | public class DistributedFileStoreCacheException : Exception 10 | { 11 | /// 12 | /// Basic exception with just a message 13 | /// 14 | /// 15 | public DistributedFileStoreCacheException(string message) : base(message){} 16 | 17 | /// 18 | /// This allows another exception be held in the innerException 19 | /// 20 | /// 21 | /// 22 | public DistributedFileStoreCacheException(string message, Exception innerException) : base(message, innerException){} 23 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/ExpirationExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.ObjectModel; 5 | using Microsoft.Extensions.Caching.Distributed; 6 | 7 | namespace Net.DistributedFileStoreCache.SupportCode; 8 | 9 | internal static class ExpirationExtensions 10 | { 11 | /// 12 | /// This sets up the timeout if a was provided 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static void SetupTimeoutIfOptions(ref CacheJsonContent cache, string key, DistributedCacheEntryOptions? entryOptions) 19 | { 20 | if (entryOptions == null) 21 | return; 22 | 23 | if (entryOptions.SlidingExpiration != null) 24 | throw new NotImplementedException("This library doesn't support sliding expirations for performance reasons."); 25 | 26 | if (entryOptions.AbsoluteExpiration != null) 27 | { 28 | //see https://stackoverflow.com/a/1688799/1434764 answer that says it uses utc 29 | cache.TimeOuts[key] = entryOptions.AbsoluteExpiration.Value.ToUniversalTime().Ticks; 30 | } 31 | else if (entryOptions.AbsoluteExpirationRelativeToNow != null) 32 | { 33 | cache.TimeOuts[key] = DateTime.UtcNow.Add( 34 | (TimeSpan)entryOptions.AbsoluteExpirationRelativeToNow!).Ticks; 35 | } 36 | } 37 | 38 | public static bool HasExpired(this long timeoutTicks) 39 | { 40 | return timeoutTicks < DateTime.UtcNow.Ticks; 41 | } 42 | 43 | /// 44 | /// This returns null if there no set value, or if it is expired. 45 | /// Otherwise it returns the value. 46 | /// 47 | /// 48 | /// 49 | /// 50 | public static string? ReturnNullIfExpires(this CacheJsonContent cache, string key) 51 | { 52 | if (!StaticCachePart.CacheContent.Cache.TryGetValue(key, out string? value)) 53 | return null; 54 | 55 | if (cache.TimeOuts.TryGetValue(key, out long timeoutTicks)) 56 | { 57 | if (timeoutTicks.HasExpired()) 58 | //it is timed out 59 | return null; 60 | } 61 | 62 | return value; 63 | } 64 | 65 | /// 66 | /// Used to return cache values with any expired values removed. 67 | /// We need this because we don't immediately update the cache file when a value expired. 68 | /// This improves the performance as write are slow when compared to the read 69 | /// 70 | /// 71 | /// 72 | public static IReadOnlyDictionary ReturnNonExpiredCacheValues(this CacheJsonContent cacheContent) 73 | { 74 | foreach (var key in cacheContent.TimeOuts.Keys.Where(key => cacheContent.TimeOuts[key].HasExpired())) 75 | { 76 | cacheContent.Cache.Remove(key); 77 | } 78 | return new ReadOnlyDictionary(cacheContent.Cache); 79 | } 80 | 81 | /// 82 | /// This removes expired cache values before writing to the cache file. 83 | /// 84 | /// 85 | public static void RemoveExpiredCacheValues(this CacheJsonContent cacheContent) 86 | { 87 | foreach (var key in cacheContent.TimeOuts.Keys.Where(key => cacheContent.TimeOuts[key].HasExpired())) 88 | { 89 | cacheContent.Cache.Remove(key); 90 | cacheContent.TimeOuts.Remove(key); 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/HandleUnauthorizedAccess.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | namespace Net.DistributedFileStoreCache.SupportCode; 5 | 6 | /// 7 | /// This contains extension methods to retry on certain exceptions 8 | /// 9 | public static class HandleUnauthorizedAccess 10 | { 11 | /// 12 | /// This will run provided action that might cause a . 13 | /// If that exception happens, then it will retry the action after a delay 14 | /// 15 | /// This contains parameters that set the delay and the number of times 16 | /// 17 | /// 18 | public static void TryAgainOnUnauthorizedAccess(this DistributedFileStoreCacheOptions fileStoreCacheOptions, Action applyAction) 19 | { 20 | var numTries = 0; 21 | var success = false; 22 | while (!success) 23 | { 24 | try 25 | { 26 | applyAction(); 27 | success = true; 28 | } 29 | catch (UnauthorizedAccessException e) 30 | { 31 | if (numTries++ > fileStoreCacheOptions.NumTriesOnUnauthorizedAccess) 32 | throw new DistributedFileStoreCacheException( 33 | "A file lock stopped this action for " + 34 | $"{fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess * fileStoreCacheOptions.NumTriesOnUnauthorizedAccess:N0} milliseconds," + 35 | " which is longer that the settings allow.", 36 | e); 37 | Thread.Sleep(fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess); 38 | } 39 | catch (AggregateException e) 40 | { 41 | if (numTries++ > fileStoreCacheOptions.NumTriesOnUnauthorizedAccess) 42 | throw new DistributedFileStoreCacheException( 43 | "Another process stopped access for " + 44 | $"{fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess * fileStoreCacheOptions.NumTriesOnUnauthorizedAccess:N0} milliseconds," + 45 | " which is longer that the settings allow.", 46 | e); 47 | Thread.Sleep(fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess); 48 | } 49 | catch (IOException e) 50 | { 51 | if (numTries++ > fileStoreCacheOptions.NumTriesOnUnauthorizedAccess) 52 | throw new DistributedFileStoreCacheException( 53 | "There was a problem on accessing the cache file for " + 54 | $"{fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess * fileStoreCacheOptions.NumTriesOnUnauthorizedAccess:N0} milliseconds," + 55 | " which is longer that the settings allow.", 56 | e); 57 | Thread.Sleep(fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess); 58 | } 59 | } 60 | } 61 | 62 | /// 63 | /// This will run provided async action that might cause a . 64 | /// If that exception happens, then it will retry the action after a delay 65 | /// 66 | /// This contains parameters that set the delay and the number of times 67 | /// 68 | /// 69 | public static async Task TryAgainOnUnauthorizedAccessAsync(this DistributedFileStoreCacheOptions fileStoreCacheOptions, 70 | Func applyActionAsync) 71 | { 72 | var numTries = 0; 73 | var success = false; 74 | while (!success) 75 | { 76 | try 77 | { 78 | await applyActionAsync(); 79 | success = true; 80 | } 81 | catch (UnauthorizedAccessException e) 82 | { 83 | if (numTries++ > fileStoreCacheOptions.NumTriesOnUnauthorizedAccess) 84 | throw new DistributedFileStoreCacheException( 85 | "A file lock stopped this action for " + 86 | $"{fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess * fileStoreCacheOptions.NumTriesOnUnauthorizedAccess:N0} milliseconds," + 87 | "which is longer that the settings allow.", 88 | e); 89 | await Task.Delay( fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess); 90 | } 91 | catch (AggregateException e) 92 | { 93 | if (numTries++ > fileStoreCacheOptions.NumTriesOnUnauthorizedAccess) 94 | throw new DistributedFileStoreCacheException( 95 | "Another process stopped access for " + 96 | $"{fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess * fileStoreCacheOptions.NumTriesOnUnauthorizedAccess:N0} milliseconds," + 97 | "which is longer that the settings allow.", 98 | e); 99 | await Task.Delay(fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess); 100 | } 101 | catch (IOException e) 102 | { 103 | if (numTries++ > fileStoreCacheOptions.NumTriesOnUnauthorizedAccess) 104 | throw new DistributedFileStoreCacheException( 105 | "There was a problem on accessing the cache file for " + 106 | $"{fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess * fileStoreCacheOptions.NumTriesOnUnauthorizedAccess:N0} milliseconds," + 107 | "which is longer that the settings allow.", 108 | e); 109 | await Task.Delay(fileStoreCacheOptions.DelayMillisecondsOnUnauthorizedAccess); 110 | } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/StaticCachePart.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("Test")] 7 | 8 | namespace Net.DistributedFileStoreCache.SupportCode; 9 | 10 | /// 11 | /// This static class contains the 12 | /// 13 | internal static class StaticCachePart 14 | { 15 | //private values 16 | private static FileSystemWatcher? _watcher; 17 | private static string? _cacheFilePathCheck; 18 | 19 | /// 20 | /// If this is true, then the cache file must be read in into the 21 | /// static dictionary 22 | /// 23 | public static bool LocalCacheIsOutOfDate { get; private set; } 24 | 25 | /// 26 | /// This contains the local static cache of the data in the cache file 27 | /// 28 | public static CacheJsonContent CacheContent { get; private set; } = new CacheJsonContent(); 29 | 30 | 31 | /// 32 | /// This should be called on startup after the has been set. 33 | /// Its job is to set up the file watcher. 34 | /// 35 | /// 36 | public static void SetupStaticCache(DistributedFileStoreCacheOptions fileStoreCacheOptions) 37 | { 38 | if (fileStoreCacheOptions.PathToCacheFileDirectory == null) 39 | throw new ArgumentNullException(nameof(fileStoreCacheOptions.PathToCacheFileDirectory)); 40 | 41 | _cacheFilePathCheck ??= fileStoreCacheOptions.FormCacheFilePath(); 42 | if (_cacheFilePathCheck != fileStoreCacheOptions.FormCacheFilePath() && !fileStoreCacheOptions.TurnOffStaticFilePathCheck) 43 | //You can only have one static 44 | throw new DistributedFileStoreCacheException( 45 | "You are trying re-registered the static cache part to a different filepath, which is not allowed."); 46 | 47 | CacheContent = new CacheJsonContent(); //Make sure empty - mainly for unit tests 48 | 49 | //Make sure there is a cache file 50 | var cacheHandler = new CacheFileHandler(fileStoreCacheOptions); 51 | cacheHandler.CreateNewCacheFileIfMissingWithRetry(); 52 | LocalCacheIsOutOfDate = true; 53 | 54 | _watcher = new FileSystemWatcher(fileStoreCacheOptions.PathToCacheFileDirectory, 55 | fileStoreCacheOptions.FormCacheFileName()); 56 | _watcher.EnableRaisingEvents = true; 57 | _watcher.NotifyFilter = NotifyFilters.LastWrite; 58 | 59 | _watcher.Changed += (sender, args) => 60 | { 61 | //when the cache file is changed, then the local 62 | LocalCacheIsOutOfDate = true; 63 | }; 64 | } 65 | 66 | /// 67 | /// This updates the local static cache parameter and sets the to false 68 | /// 69 | /// 70 | public static void UpdateLocalCache(CacheJsonContent updatedCache) 71 | { 72 | CacheContent = updatedCache; 73 | LocalCacheIsOutOfDate = false; 74 | } 75 | } -------------------------------------------------------------------------------- /Net.DistributedFileStoreCache/SupportCode/ValueTaskSyncCheckers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Runtime.ExceptionServices; 5 | 6 | namespace Net.DistributedFileStoreCache.SupportCode; 7 | 8 | internal static class ValueTaskSyncCheckers 9 | { 10 | /// 11 | /// This will check the returned 12 | /// by a method and ensure it didn't run any async methods. 13 | /// It then calls GetAwaiter().GetResult() which will 14 | /// bubble up an exception if there is one 15 | /// 16 | /// The ValueTask from a method that didn't call any async methods 17 | public static void CheckSyncValueTaskWorked(this ValueTask valueTask) 18 | { 19 | if (!valueTask.IsCompleted) 20 | throw new InvalidOperationException("Expected a sync task, but got an async task"); 21 | //Stephen Toub recommended calling GetResult every time. 22 | //This helps with pooled resources, that use the GetResult call to tell it has finished being used 23 | valueTask.GetAwaiter().GetResult(); 24 | } 25 | 26 | /// 27 | /// This will check the returned 28 | /// by a method and ensure it didn't run any async methods. 29 | /// It then calls GetAwaiter().GetResult() to return the result 30 | /// Calling .GetResult() will also bubble up an exception if there is one 31 | /// 32 | /// The ValueTask from a method that didn't call any async methods 33 | /// The result returned by the method 34 | public static TResult CheckSyncValueTaskWorkedAndReturnResult(this ValueTask valueTask) 35 | { 36 | if (!valueTask.IsCompleted) 37 | throw new InvalidOperationException("Expected a sync task, but got an async task"); 38 | return valueTask.GetAwaiter().GetResult(); 39 | } 40 | 41 | //public static TResult CheckSyncValueTaskWorkedDynamicAndReturnResult(dynamic dynamicValueType) 42 | //{ 43 | // try 44 | // { 45 | // var runner = Activator.CreateInstance(typeof(GenericValueTypeChecker), 46 | // dynamicValueType); 47 | // return ((GenericValueTypeChecker)runner).Result; 48 | // } 49 | // catch (Exception e) 50 | // { 51 | // ExceptionDispatchInfo.Capture(e?.InnerException ?? e).Throw(); 52 | // } 53 | 54 | // return default; 55 | //} 56 | 57 | //private class GenericValueTypeChecker 58 | //{ 59 | // public GenericValueTypeChecker(dynamic valueTask) 60 | // { 61 | // Result = CheckSyncValueTaskWorkedAndReturnResult(((ValueTask)valueTask)); 62 | // } 63 | 64 | // public TResult Result { get; } 65 | //} 66 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net.DistributedFileStoreCache 2 | 3 | This repo contains the Net.DistributedFileStoreCache library provides a .NET distributed cache that has two excellent features 4 | 5 | - It can get cache values blistering fast – it only takes ~25 ns. to Get one entry in a cache containing 10,000 entries. 6 | - It can also read ALL 10,000 cache entries in ~85 ns. too. 7 | - It uses a json file as the shared resource which makes it really easy to setup, and you don't need to setup / pay for a database for your cache. 8 | 9 | _NOTE: The shared resource is a json file and for it to work as a distributed cache all the application's instances must be able to access that file. This would work with Azure's Scale-Out feature with its default setup._ 10 | 11 | The main downsides is its slower than the database-bases distributed cache libraries when updating the cache values. See [Performance figures](https://github.com/JonPSmith/Net.DistributedFileStoreCache#performance-figures) for more information. 12 | 13 | The Net.DistributedFileStoreCache is an open-source library under the MIT license and a [NuGet package](https://www.nuget.org/packages/Net.DistributedFileStoreCache). The documentation can be found in the [GitHub wiki](https://github.com/JonPSmith/Net.DistributedFileStoreCache/wiki) and see the [ReleaseNotes.md](https://github.com/JonPSmith/Net.DistributedFileStoreCache/blob/main/ReleaseNotes.md) file for details of changes. 14 | 15 | ## Performance figures 16 | 17 | I measure the performance of the FileStore cache String version by the excellent BenchmarkDotNet library. My performance tests cover both reads and writes of the cache on a cache that already has 100, 1,000 and 10,000 cached values in it. 18 | 19 | Each key/value takes 37 characters and the size of the cache file are: 20 | 21 | | NumKeysValues | Cache file size| 22 | |-------------- |------------:| 23 | | 100 | 4.6 kb | 24 | | 1000 | 40.1 kb | 25 | | 10000 | 400.0 kb | 26 | 27 | BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1766 (21H1/May2021Update) 28 | Intel Core i9-9940X CPU 3.30GHz, 1 CPU, 28 logical and 14 physical cores 29 | .NET SDK=6.0.203 30 | [Host] : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT 31 | DefaultJob : .NET 6.0.7 (6.0.722.32202), X64 RyuJIT 32 | 33 | ### Read times 34 | 35 | Summary of the read side is: 36 | 37 | - Reads a single cache value took ~25 ns at all levels of cache size evaluated at. 38 | - Getting a Dictionary of ALL the cache key/values took ~80 ns at all levels of cache size evaluated at. 39 | 40 | 41 | | Method | NumKeysAtStart | Mean | Error | StdDev | 42 | |---------------- |--------------- |------------:|------------:|----------:| 43 | | GetKey | 100 | 22.69 ns | 0.367 ns | 0.343 ns | 44 | | GetAllKeyValues | 100 | 84.12 ns | 1.251 ns | 1.170 ns | 45 | | GetKey | 1000 | 21.24 ns | 0.322 ns | 0.301 ns | 46 | | GetAllKeyValues | 1000 | 81.42 ns | 1.104 ns | 1.033 ns | 47 | | GetKey | 10000 | 24.28 ns | 0.314 ns | 0.278 ns | 48 | | GetAllKeyValues | 10000 | 81.36 ns | 0.996 ns | 0.932 ns | 49 | 50 | NOTE: I tried a read of a SQL Server database containing 200 entries with Dapper and a single took about 300 us. 51 | 52 | ### Write times 53 | 54 | Summary of the write side is: 55 | 56 | - The time taken to add a cache value to cache goes up as the size of the cache is. This makes sense as unlike a database you 57 | are reading and then writing ALL the cache values into a file. 58 | - The async versions are slower than the sync versions, but it does release a thread while reading and writing. 59 | - The SetMany method takes about the same time as a single Set (see AddManyKey100 which adds 100 new entries), 60 | so use this if you have many entries to add to the cache at the same time. 61 | 62 | | Method | NumKeysAtStart | Mean | Error | StdDev | 63 | |---------------- |--------------- |------------:|----------:|----------:| 64 | | AddKey | 100 | 1,302.69 us | 9.85 us | 9.21 us | 65 | | AddManyKey100 | 100 | 1,370.46 us | 25.05 us | 23.43 us | 66 | | AddKeyAsync | 100 | 1,664.47 us | 32.51 us | 34.79 us | 67 | | AddKey | 1000 | 1,673.28 us | 25.60 us | 23.95 us | 68 | | AddManyKey100 | 1000 | 1,703.97 us | 32.31 us | 30.22 us | 69 | | AddKeyAsync | 1000 | 2,267.81 us | 45.10 us | 42.18 us | 70 | | AddKey | 10000 | 7,898.67 us | 172.10 us | 507.46 us | 71 | | AddManyKey100 | 10000 | 8,355.18 us | 166.22 us | 368.35 us | 72 | | AddKeyAsync | 10000 | 8,922.15 us | 178.30 us | 307.57 us | 73 | 74 | NOTE: I tried a write of a SQL Server database containing 200 entries with Dapper and a single INSERT took about 1,000 us. 75 | but but the real time would be longer because the SQL needs to see if a entry with the given key already exists, in 76 | which case it would need to update the value of the existing entry. 77 | -------------------------------------------------------------------------------- /ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## 2.0.0 4 | 5 | - Changed target framework to netstandard2.1 to work with any version of .NET 6 | 7 | ## 1.1.0 8 | 9 | - New Feature: SetMany / SetManyAsync now allows you to many entries in one go. This can add many entries as quickly as a single Set call. 10 | - New Feature: SetManyClass / SetManyClassAsync now allows you to many entries in one go. This can add many entries as quickly as a single SetClass call. 11 | - New Feature: ClearAll now has an option to add a list of key/value pairs into the json cache file after the cache is cleared 12 | 13 | ## 1.0.0 14 | 15 | - First release 16 | 17 | -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Test/TestData/FileStoreCacheFile.TestCacheServiceParallel.json: -------------------------------------------------------------------------------- 1 | {"Cache":{"Key4":"Diff = 1.074 ms","Key2":"Diff = 1.160 ms","Key1":"Diff = 13.030 ms","Key3":"Diff = 1.098 ms","Key5":"Diff = 1.155 ms"},"TimeOuts":{}} -------------------------------------------------------------------------------- /Test/TestData/FileStoreCacheFile.TestDistributedFileStoreCacheClass.json: -------------------------------------------------------------------------------- 1 | {"Cache":{"test":"{\"MyInt\":1,\"MyString\":\"בָּרוּךְ אַתָּה ה' אֱ-לֹהֵינוּ, מֶלֶך הָעוֹלָם\"}"},"TimeOuts":{}} -------------------------------------------------------------------------------- /Test/TestData/FileStoreCacheFile.TestDistributedFileStoreCacheClassAsync.json: -------------------------------------------------------------------------------- 1 | {"Cache":{"test":"{\"MyInt\":1,\"MyString\":\"Hello\"}"},"TimeOuts":{}} -------------------------------------------------------------------------------- /Test/TestData/FileStoreCacheFile.TestDistributedFileStoreCacheString.json: -------------------------------------------------------------------------------- 1 | {"Cache":{"test1":"first","test2":"second"},"TimeOuts":{}} -------------------------------------------------------------------------------- /Test/TestData/FileStoreCacheFile.TestDistributedFileStoreCacheString_Async.json: -------------------------------------------------------------------------------- 1 | {"Cache":{"test-timeoutExpired":"time1"},"TimeOuts":{"test-timeoutExpired":638041088939418683}} -------------------------------------------------------------------------------- /Test/TestData/FileStoreCacheFile.TestIDistributedFileStoreCacheBytes.json: -------------------------------------------------------------------------------- 1 | {"Cache":{"test":"\u0001\u0002\u0003"},"TimeOuts":{}} -------------------------------------------------------------------------------- /Test/TestData/FileStoreCacheFile.TestJsonSerializerOptions.json: -------------------------------------------------------------------------------- 1 | {"Cache":{"Test":"\u0001\u0002\u0003"},"TimeOuts":{}} -------------------------------------------------------------------------------- /Test/TestData/FileStoreCacheFile.TestMaxBytes.json: -------------------------------------------------------------------------------- 1 | {"Cache":{"Test000":"\u05D1\u05B8\u05BC\u05E8\u05D5\u05BC\u05DA\u05B0 \u05D0\u05B7\u05EA\u05B8\u05BC\u05D4 \u05D4\u0027 \u05D0\u05B1-\u05DC\u05B9\u05D4\u05B5\u05D9\u05E0\u05D5\u05BC, \u05DE\u05B6\u05DC\u05B6\u05DA \u05D4\u05B8\u05E2\u05D5\u05B9\u05DC\u05B8\u05DD","Test001":"\u05D1\u05B8\u05BC\u05E8\u05D5\u05BC\u05DA\u05B0 \u05D0\u05B7\u05EA\u05B8\u05BC\u05D4 \u05D4\u0027 \u05D0\u05B1-\u05DC\u05B9\u05D4\u05B5\u05D9\u05E0\u05D5\u05BC, \u05DE\u05B6\u05DC\u05B6\u05DA \u05D4\u05B8\u05E2\u05D5\u05B9\u05DC\u05B8\u05DD","Test002":"\u05D1\u05B8\u05BC\u05E8\u05D5\u05BC\u05DA\u05B0 \u05D0\u05B7\u05EA\u05B8\u05BC\u05D4 \u05D4\u0027 \u05D0\u05B1-\u05DC\u05B9\u05D4\u05B5\u05D9\u05E0\u05D5\u05BC, \u05DE\u05B6\u05DC\u05B6\u05DA \u05D4\u05B8\u05E2\u05D5\u05B9\u05DC\u05B8\u05DD","Test003":"\u05D1\u05B8\u05BC\u05E8\u05D5\u05BC\u05DA\u05B0 \u05D0\u05B7\u05EA\u05B8\u05BC\u05D4 \u05D4\u0027 \u05D0\u05B1-\u05DC\u05B9\u05D4\u05B5\u05D9\u05E0\u05D5\u05BC, \u05DE\u05B6\u05DC\u05B6\u05DA \u05D4\u05B8\u05E2\u05D5\u05B9\u05DC\u05B8\u05DD","Test004":"\u05D1\u05B8\u05BC\u05E8\u05D5\u05BC\u05DA\u05B0 \u05D0\u05B7\u05EA\u05B8\u05BC\u05D4 \u05D4\u0027 \u05D0\u05B1-\u05DC\u05B9\u05D4\u05B5\u05D9\u05E0\u05D5\u05BC, \u05DE\u05B6\u05DC\u05B6\u05DA \u05D4\u05B8\u05E2\u05D5\u05B9\u05DC\u05B8\u05DD"},"TimeOuts":{}} -------------------------------------------------------------------------------- /Test/TestData/FileStoreCacheFile.TestStaticCachePart.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cache": { 3 | "test": "goodbye" 4 | } 5 | } -------------------------------------------------------------------------------- /Test/TestData/TestJsonSerializerStream.json: -------------------------------------------------------------------------------- 1 | {"Cache":{"test":"does it work"},"TimeOuts":{}} -------------------------------------------------------------------------------- /Test/TestData/testlock.2.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cache": {} 3 | } -------------------------------------------------------------------------------- /Test/TestData/testlock.json: -------------------------------------------------------------------------------- 1 | { 2 | "Cache": {} 3 | } -------------------------------------------------------------------------------- /Test/TestHelpers/DisplayExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text.Json; 5 | using Net.DistributedFileStoreCache; 6 | using Net.DistributedFileStoreCache.SupportCode; 7 | using Xunit.Abstractions; 8 | 9 | namespace Test.TestHelpers; 10 | 11 | public static class DisplayExtensions 12 | { 13 | public static CacheJsonContent GetCacheFileContentAsJson(this DistributedFileStoreCacheOptions options) 14 | { 15 | var cacheFilePath = options.FormCacheFilePath(); 16 | if (!File.Exists(cacheFilePath)) 17 | throw new Exception("No cache file found!"); 18 | 19 | var jsonString = File.ReadAllText(cacheFilePath); 20 | var cacheContent = JsonSerializer.Deserialize(jsonString)!; 21 | 22 | return cacheContent; 23 | } 24 | 25 | public static void DisplayCacheFile(this DistributedFileStoreCacheOptions options, ITestOutputHelper output) 26 | { 27 | var cacheFilePath = options.FormCacheFilePath(); 28 | options.TryAgainOnUnauthorizedAccess(() => 29 | { 30 | if (!File.Exists(cacheFilePath)) 31 | { 32 | output.WriteLine($"No cache file called {Path.GetFileName(cacheFilePath)} was found."); 33 | return; 34 | } 35 | 36 | var text = File.ReadAllText(cacheFilePath); 37 | output.WriteLine(text); 38 | }); 39 | } 40 | } -------------------------------------------------------------------------------- /Test/TestHelpers/ParallelExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks.Dataflow; 5 | 6 | namespace Test.TestHelpers; 7 | 8 | //NOTE Parallel.ForEach doesn't handle async 9 | //I found a very useful article https://medium.com/@alex.puiu/parallel-foreach-async-in-c-36756f8ebe62 10 | //From this I decided the AsyncParallelForEach approach, which can run async methods in paralle 11 | public static class ParallelExtensions 12 | { 13 | public static async IAsyncEnumerable NumTimesAsyncEnumerable(this int numTimes) 14 | { 15 | for (int i = 1; i <= numTimes; i++) 16 | { 17 | yield return i; 18 | } 19 | } 20 | 21 | 22 | public static async Task AsyncParallelForEach(this IAsyncEnumerable source, Func body, 23 | int maxDegreeOfParallelism = DataflowBlockOptions.Unbounded, TaskScheduler scheduler = null) 24 | { 25 | var options = new ExecutionDataflowBlockOptions 26 | { 27 | MaxDegreeOfParallelism = maxDegreeOfParallelism 28 | }; 29 | if (scheduler != null) 30 | options.TaskScheduler = scheduler; 31 | 32 | var block = new ActionBlock(body, options); 33 | 34 | await foreach (var item in source) 35 | block.Post(item); 36 | 37 | block.Complete(); 38 | await block.Completion; 39 | } 40 | } -------------------------------------------------------------------------------- /Test/TestHelpers/StubFileStoreCacheClass.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text.Encodings.Web; 5 | using System.Text.Json; 6 | using Microsoft.Extensions.Caching.Distributed; 7 | using Net.DistributedFileStoreCache; 8 | 9 | namespace Test.TestHelpers; 10 | 11 | public class StubFileStoreCacheClass : StubFileStoreCacheString, IDistributedFileStoreCacheClass 12 | { 13 | private readonly JsonSerializerOptions _jsonOptions = new JsonSerializerOptions 14 | { 15 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping 16 | }; 17 | 18 | /// 19 | /// This method is useful if you want to decode a cache value via the 20 | /// or the methods 21 | /// 22 | /// A class which can be created 23 | /// 24 | /// The deserialize class or null. 25 | public T? GetClassFromString(string? jsonString) where T : class, new() 26 | { 27 | return jsonString == null ? null : JsonSerializer.Deserialize(jsonString); 28 | } 29 | 30 | /// Gets a class stored as json linked to the given key. 31 | /// A string identifying the requested stored class. 32 | /// A class which can be created 33 | /// The deserialize class or null. 34 | public T? GetClass(string key) where T : class, new() 35 | { 36 | var stringValue = Get(key); 37 | return stringValue == null ? null : JsonSerializer.Deserialize(stringValue); 38 | } 39 | 40 | /// Gets a class stored as json linked to the given key. 41 | /// A string identifying the requested stored class. 42 | /// Optional. The used to propagate notifications that the operation should be canceled. 43 | /// A class which can be created 44 | /// The located class or null withing a Task result. 45 | public async Task GetClassAsync(string key, CancellationToken token = new CancellationToken()) where T : class, new() 46 | { 47 | var stringValue = await GetAsync(key, token); 48 | return stringValue == null ? null : JsonSerializer.Deserialize(stringValue); 49 | } 50 | 51 | /// Serializers the class and stores the json against the given key. 52 | /// A string identifying the requested value. 53 | /// The class that you wanted to be stored in the cache. 54 | /// The cache options for the value. 55 | /// A class which can be created 56 | public void SetClass(string key, T yourClass, DistributedCacheEntryOptions? options = null) where T : class, new() 57 | { 58 | var jsonString = JsonSerializer.Serialize(yourClass, _jsonOptions); 59 | Set(key, jsonString, options); 60 | } 61 | 62 | /// Serializers the class and stores the json against the given key. 63 | /// A string identifying the requested value. 64 | /// The class that you wanted to be stored in the cache. 65 | /// The cache options for the value. 66 | /// Optional. The used to propagate notifications that the operation should be canceled. 67 | /// A class which can be created 68 | public async Task SetClassAsync(string key, T yourClass, DistributedCacheEntryOptions? options = null, 69 | CancellationToken token = new ()) where T : class, new() 70 | { 71 | var jsonString = JsonSerializer.Serialize(yourClass, _jsonOptions); 72 | await SetAsync(key, jsonString, options, token); 73 | } 74 | 75 | /// Serializes all the values in each KeyValue using the T type and save each into the cache 76 | /// List of KeyValuePairs to be added to the cache, with the values being serialized. 77 | /// Optional: The cache options for the value. 78 | /// A class which contains the data to stored as JSON in the cache 79 | public void SetManyClass(List> manyEntries, DistributedCacheEntryOptions? options) where T : class, new() 80 | { 81 | foreach (var keyValue in manyEntries) 82 | { 83 | var jsonString = JsonSerializer.Serialize(keyValue.Value, _jsonOptions); 84 | Set(keyValue.Key, jsonString, options); 85 | } 86 | } 87 | 88 | /// Serializes all the values in each KeyValue using the T type and save each into the cache 89 | /// List of KeyValuePairs to be added to the cache, with the values being serialized. 90 | /// Optional: The cache options for the value. 91 | /// Optional. The used to propagate notifications that the operation should be canceled. 92 | /// A class which contains the data to stored as JSON in the cache 93 | public async Task SetManyClassAsync(List> manyEntries, DistributedCacheEntryOptions? options, CancellationToken token) where T : class, new() 94 | { 95 | foreach (var keyValue in manyEntries) 96 | { 97 | var jsonString = JsonSerializer.Serialize(keyValue.Value, _jsonOptions); 98 | await SetAsync(keyValue.Key, jsonString, options, token); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /Test/TestHelpers/StubFileStoreCacheString.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Collections.ObjectModel; 5 | using Microsoft.Extensions.Caching.Distributed; 6 | using Net.DistributedFileStoreCache; 7 | 8 | namespace Test.TestHelpers; 9 | 10 | public class StubFileStoreCacheString : IDistributedFileStoreCacheString 11 | { 12 | private Dictionary _cache = new Dictionary(); 13 | 14 | /// Gets a value with the given key. 15 | /// A string identifying the requested value. 16 | /// The located value or null. 17 | public string? Get(string key) 18 | { 19 | return _cache.TryGetValue(key, out string? value) ? value : null; 20 | } 21 | 22 | /// Gets a value with the given key. 23 | /// A string identifying the requested value. 24 | /// Optional. The used to propagate notifications that the operation should be canceled. 25 | /// The that represents the asynchronous operation, containing the located value or null. 26 | public Task GetAsync(string key, CancellationToken token = new CancellationToken()) 27 | { 28 | return Task.FromResult(_cache.TryGetValue(key, out string? value) ? value : null); 29 | } 30 | 31 | /// Sets a value with the given key. 32 | /// A string identifying the requested value. 33 | /// The value to set in the cache. 34 | /// The cache options for the value. 35 | public void Set(string key, string value, DistributedCacheEntryOptions? options = null) 36 | { 37 | _cache[key] = value; 38 | } 39 | 40 | /// Sets the value with the given key. 41 | /// A string identifying the requested value. 42 | /// The value to set in the cache. 43 | /// The cache options for the value. 44 | /// Optional. The used to propagate notifications that the operation should be canceled. 45 | /// The that represents the asynchronous operation. 46 | public Task SetAsync(string key, string value, DistributedCacheEntryOptions? options = null, 47 | CancellationToken token = new CancellationToken()) 48 | { 49 | _cache[key] = value; 50 | return Task.CompletedTask; 51 | } 52 | 53 | /// Sets many entries via a list of KeyValues 54 | /// List of KeyValuePairs to be added to the cache. 55 | /// Optional: The cache options for the value. 56 | public void SetMany(List> manyEntries, DistributedCacheEntryOptions? options = null) 57 | { 58 | foreach (var keyValuePair in manyEntries) 59 | { 60 | _cache[keyValuePair.Key] = keyValuePair.Value; 61 | } 62 | } 63 | 64 | /// Sets many entries via a list of KeyValues 65 | /// List of KeyValuePairs to be added to the cache. 66 | /// Optional: The cache options for the value. 67 | /// Optional. The used to propagate notifications that the operation should be canceled. 68 | public Task SetManyAsync(List> manyEntries, DistributedCacheEntryOptions? options = null, 69 | CancellationToken token = new CancellationToken()) 70 | { 71 | foreach (var keyValuePair in manyEntries) 72 | { 73 | _cache[keyValuePair.Key] = keyValuePair.Value; 74 | } 75 | return Task.CompletedTask; 76 | } 77 | 78 | /// Removes the value with the given key. 79 | /// A string identifying the requested value. 80 | public void Remove(string key) 81 | { 82 | _cache.Remove(key); 83 | } 84 | 85 | /// Removes the value with the given key. 86 | /// A string identifying the requested value. 87 | /// Optional. The used to propagate notifications that the operation should be canceled. 88 | /// The that represents the asynchronous operation. 89 | public Task RemoveAsync(string key, CancellationToken token = new CancellationToken()) 90 | { 91 | _cache.Remove(key); 92 | return Task.CompletedTask; 93 | } 94 | 95 | /// 96 | /// This clears all the key/value pairs from the json cache file, with option to add entries after the cache is cleared. 97 | /// 98 | /// Optional: After of the clearing the cache these KeyValues will written into the cache 99 | /// Optional: If there are entries to add to the cache, this will set the timeout time. 100 | public void ClearAll(List>? manyEntries = null, DistributedCacheEntryOptions? entryOptions = null) 101 | { 102 | _cache = new Dictionary(); 103 | if (manyEntries == null) 104 | return; 105 | 106 | foreach (var keyValuePair in manyEntries) 107 | { 108 | _cache[keyValuePair.Key] = keyValuePair.Value; 109 | } 110 | } 111 | 112 | 113 | /// 114 | /// This return all the cached values as a dictionary 115 | /// 116 | /// 117 | public IReadOnlyDictionary GetAllKeyValues() 118 | { 119 | return new ReadOnlyDictionary(_cache); 120 | } 121 | 122 | /// 123 | /// This return all the cached values as a dictionary 124 | /// 125 | /// 126 | public Task> GetAllKeyValuesAsync(CancellationToken token = new CancellationToken()) 127 | { 128 | return Task.FromResult(new ReadOnlyDictionary(_cache) as IReadOnlyDictionary); 129 | } 130 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestCacheFileExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Net.DistributedFileStoreCache; 5 | using Net.DistributedFileStoreCache.SupportCode; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | using Xunit.Extensions.AssertExtensions; 9 | 10 | namespace Test.UnitTests; 11 | 12 | public class TestCacheFileExtensions 13 | { 14 | private readonly ITestOutputHelper _output; 15 | 16 | public TestCacheFileExtensions(ITestOutputHelper output) 17 | { 18 | _output = output; 19 | } 20 | 21 | 22 | [Fact] 23 | public void TestFormCacheFileName() 24 | { 25 | //SETUP 26 | var options = new DistributedFileStoreCacheOptions 27 | { 28 | FirstPartOfCacheFileName = "Test", 29 | SecondPartOfCacheFileName = "Type" 30 | }; 31 | 32 | //ATTEMPT 33 | var fileName = options.FormCacheFileName(); 34 | 35 | //VERIFY 36 | fileName.ShouldEqual("Test.Type.json"); 37 | } 38 | 39 | [Fact] 40 | public void TestFormCacheFilePath() 41 | { 42 | //SETUP 43 | var options = new DistributedFileStoreCacheOptions 44 | { 45 | FirstPartOfCacheFileName = "Test", 46 | SecondPartOfCacheFileName = "Type", 47 | PathToCacheFileDirectory = "C:\\directory\\" 48 | }; 49 | 50 | //ATTEMPT 51 | var fileName = options.FormCacheFilePath(); 52 | 53 | //VERIFY 54 | fileName.ShouldEqual("C:\\directory\\Test.Type.json"); 55 | } 56 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestCacheServiceParallel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Net.DistributedFileStoreCache; 6 | using Test.TestHelpers; 7 | using TestSupport.Helpers; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | using Xunit.Extensions.AssertExtensions; 11 | 12 | namespace Test.UnitTests; 13 | 14 | // see https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel 15 | [Collection("Sequential")] 16 | public class TestCacheServiceParallel 17 | { 18 | private readonly ITestOutputHelper _output; 19 | private DistributedFileStoreCacheOptions _options; 20 | 21 | /// Initializes a new instance of the class. 22 | public TestCacheServiceParallel(ITestOutputHelper output) 23 | { 24 | _output = output; 25 | } 26 | 27 | private IDistributedFileStoreCacheString SetupDistributedFileStoreCache() 28 | { 29 | var services = new ServiceCollection(); 30 | _options = services.AddDistributedFileStoreCache(options => 31 | { 32 | options.WhichVersion = FileStoreCacheVersions.String; 33 | options.PathToCacheFileDirectory = TestData.GetTestDataDir(); 34 | options.SecondPartOfCacheFileName = GetType().Name; 35 | options.TurnOffStaticFilePathCheck = true; 36 | }); 37 | var serviceProvider = services.BuildServiceProvider(); 38 | return serviceProvider.GetRequiredService(); 39 | } 40 | 41 | [Fact] 42 | public void TestRunTwoServices() 43 | { 44 | //SETUP 45 | var cache1 = SetupDistributedFileStoreCache(); 46 | var cache2 = SetupDistributedFileStoreCache(); 47 | cache1.ClearAll(); 48 | 49 | //ATTEMPT 50 | cache1.Set("Cache1", DateTime.UtcNow.ToString("O"), null); 51 | cache2.Set("Cache2", DateTime.UtcNow.ToString("O"), null); 52 | 53 | //VERIFY 54 | cache1.GetAllKeyValues().Keys.ShouldEqual(new []{ "Cache1", "Cache2" }); 55 | } 56 | 57 | [Fact] 58 | public void TestUpdateInParallelWithDelays() 59 | { 60 | //SETUP 61 | SetupDistributedFileStoreCache().ClearAll(); 62 | var startDate = DateTime.Now; 63 | 64 | //ATTEMPT 65 | Parallel.ForEach(Enumerable.Range(1, 5), 66 | currentElement => 67 | { 68 | Task.Delay(10 * currentElement); 69 | var distributedCache = SetupDistributedFileStoreCache(); 70 | distributedCache.Set($"Key{currentElement}", 71 | $"Diff = {DateTime.Now.Subtract(startDate).TotalMilliseconds:F3} ms", null); 72 | }); 73 | 74 | //VERIFY 75 | _options.DisplayCacheFile(_output); 76 | } 77 | 78 | [Fact] 79 | public void TestUpdateInParallel() 80 | { 81 | //SETUP 82 | SetupDistributedFileStoreCache().ClearAll(); 83 | var startDate = DateTime.Now; 84 | 85 | //ATTEMPT 86 | Parallel.ForEach(Enumerable.Range(1, 5), 87 | currentElement => 88 | { 89 | var distributedCache = SetupDistributedFileStoreCache(); 90 | distributedCache.Set($"Key{currentElement}", 91 | $"Diff = {DateTime.Now.Subtract(startDate).TotalMilliseconds:F3} ms", null); 92 | }); 93 | 94 | 95 | //VERIFY 96 | _options.DisplayCacheFile(_output); 97 | } 98 | 99 | [Fact] 100 | public async Task TestUpdateInParallelWithDelaysAsync() 101 | { 102 | //SETUP 103 | SetupDistributedFileStoreCache().ClearAll(); 104 | var startDate = DateTime.Now; 105 | 106 | async Task TaskAsync(int num) 107 | { 108 | await Task.Delay(10 * num); 109 | var distributedCache = SetupDistributedFileStoreCache(); 110 | await distributedCache.SetAsync($"Key{num}", 111 | $"Diff = {DateTime.Now.Subtract(startDate).TotalMilliseconds:F3} ms", null); 112 | } 113 | 114 | //ATTEMPT 115 | await 5.NumTimesAsyncEnumerable().AsyncParallelForEach(TaskAsync); 116 | 117 | //VERIFY 118 | _options.DisplayCacheFile(_output); 119 | } 120 | 121 | [Fact] 122 | public async Task TestUpdateInParallelAsync() 123 | { 124 | //SETUP 125 | SetupDistributedFileStoreCache().ClearAll(); 126 | var startDate = DateTime.Now; 127 | 128 | async Task TaskAsync(int num) 129 | { 130 | var distributedCache = SetupDistributedFileStoreCache(); 131 | await distributedCache.SetAsync($"Key{num}", 132 | $"Diff = {DateTime.Now.Subtract(startDate).TotalMilliseconds:F3} ms", null); 133 | } 134 | 135 | //ATTEMPT 136 | await 5.NumTimesAsyncEnumerable().AsyncParallelForEach(TaskAsync); 137 | 138 | //VERIFY 139 | _options.DisplayCacheFile(_output); 140 | } 141 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestDistributedFileStoreCacheClass.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Net.DistributedFileStoreCache; 6 | using Test.TestHelpers; 7 | using TestSupport.Helpers; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | using Xunit.Extensions.AssertExtensions; 11 | 12 | namespace Test.UnitTests; 13 | 14 | // see https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel 15 | [Collection("Sequential")] 16 | public class TestDistributedFileStoreCacheClass 17 | { 18 | private readonly IDistributedFileStoreCacheClass _fsCache; 19 | private readonly DistributedFileStoreCacheOptions _options; 20 | private readonly ITestOutputHelper _output; 21 | 22 | public TestDistributedFileStoreCacheClass(ITestOutputHelper output) 23 | { 24 | _output = output; 25 | 26 | var services = new ServiceCollection(); 27 | _options = services.AddDistributedFileStoreCache(options => 28 | { 29 | options.WhichVersion = FileStoreCacheVersions.Class; 30 | options.PathToCacheFileDirectory = TestData.GetTestDataDir(); 31 | options.SecondPartOfCacheFileName = GetType().Name; 32 | options.TurnOffStaticFilePathCheck = true; 33 | }); 34 | var serviceProvider = services.BuildServiceProvider(); 35 | 36 | _fsCache = serviceProvider.GetRequiredService(); 37 | } 38 | 39 | private class JsonClass1 40 | { 41 | public int MyInt { get; set; } 42 | public string MyString { get; set; } 43 | } 44 | 45 | private class JsonClass2 46 | { 47 | public JsonClass1 MyClass1 { get; set; } 48 | public int MyInt2 { get; set; } 49 | } 50 | 51 | [Fact] 52 | public void DistributedFileStoreCacheSetClass_SetJsonClass1() 53 | { 54 | //SETUP 55 | _fsCache.ClearAll(); 56 | 57 | //ATTEMPT 58 | _fsCache.SetClass("test", new JsonClass1{MyInt = 1, MyString = "Hello"}); 59 | 60 | //VERIFY 61 | var allValues = _fsCache.GetAllKeyValues(); 62 | allValues.Count.ShouldEqual(1); 63 | allValues["test"].ShouldEqual("{\"MyInt\":1,\"MyString\":\"Hello\"}"); 64 | 65 | _options.DisplayCacheFile(_output); 66 | } 67 | 68 | [Fact] 69 | public void DistributedFileStoreCacheSetClass_GetClassFromStringJsonClass1() 70 | { 71 | //SETUP 72 | _fsCache.ClearAll(); 73 | _fsCache.SetClass("test", new JsonClass1 { MyInt = 1, MyString = "Hello" }); 74 | 75 | //ATTEMPT 76 | var allValuesDict = _fsCache.GetAllKeyValues(); 77 | var jsonClass1 = _fsCache.GetClassFromString(allValuesDict ["test"]); 78 | 79 | //VERIFY 80 | jsonClass1.ShouldBeType(); 81 | jsonClass1.ShouldNotBeNull(); 82 | jsonClass1.MyInt.ShouldEqual(1); 83 | jsonClass1.MyString.ShouldEqual("Hello"); 84 | _options.DisplayCacheFile(_output); 85 | } 86 | 87 | [Fact] 88 | public void DistributedFileStoreCacheSetClass_GetJsonClass1() 89 | { 90 | //SETUP 91 | _fsCache.ClearAll(); 92 | _fsCache.SetClass("test", new JsonClass1 { MyInt = 1, MyString = "Hello" }); 93 | 94 | //ATTEMPT 95 | var jsonClass1 = _fsCache.GetClass("test"); 96 | 97 | //VERIFY 98 | jsonClass1.ShouldBeType(); 99 | jsonClass1.ShouldNotBeNull(); 100 | jsonClass1.MyInt.ShouldEqual(1); 101 | jsonClass1.MyString.ShouldEqual("Hello"); 102 | _options.DisplayCacheFile(_output); 103 | } 104 | 105 | [Fact] 106 | public void DistributedFileStoreCacheSetClass_Bad() 107 | { 108 | //SETUP 109 | _fsCache.ClearAll(); 110 | _fsCache.SetClass("test", new JsonClass1 { MyInt = 1, MyString = "Hello" }); 111 | 112 | //ATTEMPT 113 | var jsonClass2 = _fsCache.GetClass("test"); 114 | 115 | //VERIFY 116 | jsonClass2.ShouldBeType(); 117 | jsonClass2.ShouldNotBeNull(); 118 | jsonClass2.MyInt2.ShouldEqual(default); 119 | jsonClass2.MyClass1.ShouldEqual(null); 120 | } 121 | 122 | [Fact] 123 | public void DistributedFileStoreCacheSetClass_JsonClass2() 124 | { 125 | //SETUP 126 | _fsCache.ClearAll(); 127 | 128 | //ATTEMPT 129 | _fsCache.SetClass("test", new JsonClass2 { MyInt2 = 3, MyClass1 = new JsonClass1{ MyInt = 1, MyString = "Hello" } }); 130 | 131 | //VERIFY 132 | var allValues = _fsCache.GetAllKeyValues(); 133 | allValues.Count.ShouldEqual(1); 134 | allValues["test"].ShouldEqual("{\"MyClass1\":{\"MyInt\":1,\"MyString\":\"Hello\"},\"MyInt2\":3}"); 135 | 136 | 137 | _options.DisplayCacheFile(_output); 138 | } 139 | 140 | [Fact] 141 | public void DistributedFileStoreCacheSetClass_Unicode() 142 | { 143 | //SETUP 144 | _fsCache.ClearAll(); 145 | var unicode = "בָּרוּךְ אַתָּה ה' אֱ-לֹהֵינוּ, מֶלֶך הָעוֹלָם"; 146 | 147 | //ATTEMPT 148 | _fsCache.SetClass("test", new JsonClass1 { MyInt = 1, MyString = unicode }); 149 | 150 | //VERIFY 151 | var allValues = _fsCache.GetAllKeyValues(); 152 | allValues.Count.ShouldEqual(1); 153 | allValues["test"].ShouldEqual("{\"MyInt\":1,\"MyString\":\"" + unicode + "\"}"); 154 | 155 | 156 | _options.DisplayCacheFile(_output); 157 | } 158 | 159 | [Fact] 160 | public void DistributedFileStoreCacheSetClass_JsonClass_Example() 161 | { 162 | //SETUP 163 | _fsCache.ClearAll(); 164 | 165 | //ATTEMPT 166 | _fsCache.SetClass("test", new JsonClass2 { MyInt2 = 3, 167 | MyClass1 = new JsonClass1 { MyInt = 1, MyString = "Hello" } }); 168 | 169 | //VERIFY 170 | _fsCache.Get("test").ShouldEqual( 171 | "{\"MyClass1\":{\"MyInt\":1,\"MyString\":\"Hello\"},\"MyInt2\":3}"); 172 | var jsonClass2 = _fsCache.GetClass("test"); 173 | jsonClass2.ShouldBeType(); 174 | jsonClass2.ShouldNotBeNull(); 175 | jsonClass2.MyInt2.ShouldEqual(3); 176 | jsonClass2.MyClass1.ShouldNotBeNull(); 177 | jsonClass2.MyClass1.MyInt.ShouldEqual(1); 178 | jsonClass2.MyClass1.MyString.ShouldEqual("Hello"); 179 | 180 | _options.DisplayCacheFile(_output); 181 | //Example if no UnsafeRelaxedJsonEscaping 182 | //"{\u0022MyClass1\u0022:{\u0022MyInt\u0022:1,\u0022MyString\u0022:\u0022Hello\u0022},\u0022MyInt\u0022:3}"} 183 | } 184 | 185 | [Fact] 186 | public void DistributedFileStoreCacheSetManyClass_JsonClass2() 187 | { 188 | //SETUP 189 | _fsCache.ClearAll(); 190 | 191 | //ATTEMPT 192 | _fsCache.SetManyClass(new List> 193 | { 194 | new("test1", new JsonClass1 { MyInt = 1, MyString = "Hello" }), 195 | new("test2", new JsonClass1 { MyInt = 2, MyString = "Goodbye" }) 196 | }); 197 | 198 | //VERIFY 199 | var allValues = _fsCache.GetAllKeyValues(); 200 | allValues.Count.ShouldEqual(2); 201 | allValues["test1"].ShouldEqual("{\"MyInt\":1,\"MyString\":\"Hello\"}"); 202 | allValues["test2"].ShouldEqual("{\"MyInt\":2,\"MyString\":\"Goodbye\"}"); 203 | 204 | _options.DisplayCacheFile(_output); 205 | } 206 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestDistributedFileStoreCacheClassAsync.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Net.DistributedFileStoreCache; 6 | using Test.TestHelpers; 7 | using TestSupport.Helpers; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | using Xunit.Extensions.AssertExtensions; 11 | 12 | namespace Test.UnitTests; 13 | 14 | // see https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel 15 | [Collection("Sequential")] 16 | public class TestDistributedFileStoreCacheClassAsync 17 | { 18 | private readonly IDistributedFileStoreCacheClass _fsCache; 19 | private readonly DistributedFileStoreCacheOptions _options; 20 | private readonly ITestOutputHelper _output; 21 | 22 | public TestDistributedFileStoreCacheClassAsync(ITestOutputHelper output) 23 | { 24 | _output = output; 25 | 26 | var services = new ServiceCollection(); 27 | _options = services.AddDistributedFileStoreCache(options => 28 | { 29 | options.WhichVersion = FileStoreCacheVersions.Class; 30 | options.PathToCacheFileDirectory = TestData.GetTestDataDir(); 31 | options.SecondPartOfCacheFileName = GetType().Name; 32 | options.TurnOffStaticFilePathCheck = true; 33 | }); 34 | var serviceProvider = services.BuildServiceProvider(); 35 | 36 | _fsCache = serviceProvider.GetRequiredService(); 37 | } 38 | 39 | private class JsonClass1 40 | { 41 | public int MyInt { get; set; } 42 | public string MyString { get; set; } 43 | } 44 | 45 | 46 | [Fact] 47 | public async Task DistributedFileStoreCacheSetClass_SetJsonClass1() 48 | { 49 | //SETUP 50 | _fsCache.ClearAll(); 51 | 52 | //ATTEMPT 53 | await _fsCache.SetClassAsync("test", new JsonClass1{MyInt = 1, MyString = "Hello"}); 54 | 55 | //VERIFY 56 | var allValues = _fsCache.GetAllKeyValues(); 57 | allValues.Count.ShouldEqual(1); 58 | allValues["test"].ShouldEqual("{\"MyInt\":1,\"MyString\":\"Hello\"}"); 59 | 60 | _options.DisplayCacheFile(_output); 61 | } 62 | 63 | [Fact] 64 | public async Task DistributedFileStoreCacheSetClass_GetClassFromStringJsonClass1() 65 | { 66 | //SETUP 67 | _fsCache.ClearAll(); 68 | await _fsCache.SetClassAsync("test", new JsonClass1 { MyInt = 1, MyString = "Hello" }); 69 | 70 | //ATTEMPT 71 | var allValuesDict = _fsCache.GetAllKeyValues(); 72 | var jsonClass1 = _fsCache.GetClassFromString(allValuesDict ["test"]); 73 | 74 | //VERIFY 75 | jsonClass1.ShouldBeType(); 76 | jsonClass1.ShouldNotBeNull(); 77 | jsonClass1.MyInt.ShouldEqual(1); 78 | jsonClass1.MyString.ShouldEqual("Hello"); 79 | _options.DisplayCacheFile(_output); 80 | } 81 | 82 | [Fact] 83 | public async Task DistributedFileStoreCacheSetClass_GetJsonClass1() 84 | { 85 | //SETUP 86 | _fsCache.ClearAll(); 87 | await _fsCache.SetClassAsync("test", new JsonClass1 { MyInt = 1, MyString = "Hello" }); 88 | 89 | //ATTEMPT 90 | var jsonClass1 = _fsCache.GetClass("test"); 91 | 92 | //VERIFY 93 | jsonClass1.ShouldBeType(); 94 | jsonClass1.ShouldNotBeNull(); 95 | jsonClass1.MyInt.ShouldEqual(1); 96 | jsonClass1.MyString.ShouldEqual("Hello"); 97 | _options.DisplayCacheFile(_output); 98 | } 99 | 100 | [Fact] 101 | public async Task DistributedFileStoreCacheSetManyClass_JsonClass2() 102 | { 103 | //SETUP 104 | _fsCache.ClearAll(); 105 | 106 | //ATTEMPT 107 | await _fsCache.SetManyClassAsync(new List> 108 | { 109 | new("test1", new JsonClass1 { MyInt = 1, MyString = "Hello" }), 110 | new("test2", new JsonClass1 { MyInt = 2, MyString = "Goodbye" }) 111 | }); 112 | 113 | //VERIFY 114 | var allValues = _fsCache.GetAllKeyValues(); 115 | allValues.Count.ShouldEqual(2); 116 | allValues["test1"].ShouldEqual("{\"MyInt\":1,\"MyString\":\"Hello\"}"); 117 | allValues["test2"].ShouldEqual("{\"MyInt\":2,\"MyString\":\"Goodbye\"}"); 118 | 119 | _options.DisplayCacheFile(_output); 120 | } 121 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestDistributedFileStoreCacheString.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.Caching.Distributed; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Net.DistributedFileStoreCache; 7 | using Net.DistributedFileStoreCache.SupportCode; 8 | using Test.TestHelpers; 9 | using TestSupport.Helpers; 10 | using Xunit; 11 | using Xunit.Abstractions; 12 | using Xunit.Extensions.AssertExtensions; 13 | 14 | namespace Test.UnitTests; 15 | 16 | // see https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel 17 | [Collection("Sequential")] 18 | public class TestDistributedFileStoreCacheString 19 | { 20 | private readonly IDistributedFileStoreCacheString _distributedCache; 21 | private readonly DistributedFileStoreCacheOptions _options; 22 | private readonly ITestOutputHelper _output; 23 | 24 | public TestDistributedFileStoreCacheString(ITestOutputHelper output) 25 | { 26 | _output = output; 27 | 28 | var services = new ServiceCollection(); 29 | _options = services.AddDistributedFileStoreCache(options => 30 | { 31 | options.WhichVersion = FileStoreCacheVersions.String; 32 | options.PathToCacheFileDirectory = TestData.GetTestDataDir(); 33 | options.SecondPartOfCacheFileName = GetType().Name; 34 | options.TurnOffStaticFilePathCheck = true; 35 | }); 36 | var serviceProvider = services.BuildServiceProvider(); 37 | 38 | _distributedCache = serviceProvider.GetRequiredService(); 39 | } 40 | 41 | [Fact] 42 | public void DistributedFileStoreCacheEmpty() 43 | { 44 | //SETUP 45 | _distributedCache.ClearAll(); 46 | 47 | //ATTEMPT 48 | var value = _distributedCache.Get("test"); 49 | 50 | //VERIFY 51 | value.ShouldBeNull(); 52 | _distributedCache.GetAllKeyValues().Count.ShouldEqual(0); 53 | 54 | _options.DisplayCacheFile(_output); 55 | } 56 | 57 | [Fact] 58 | public void DistributedFileStoreClearAllWithEntries() 59 | { 60 | //SETUP 61 | _distributedCache.ClearAll(); 62 | _distributedCache.Set("old", "entry"); 63 | 64 | //ATTEMPT 65 | _distributedCache.ClearAll((new List> 66 | { 67 | new ("test1", "first"), 68 | new ("test2", "second") 69 | })); 70 | 71 | //VERIFY 72 | var allValues = _distributedCache.GetAllKeyValues(); 73 | allValues.Count.ShouldEqual(2); 74 | allValues["test1"].ShouldEqual("first"); 75 | allValues["test2"].ShouldEqual("second"); 76 | 77 | _options.DisplayCacheFile(_output); 78 | } 79 | 80 | [Fact] 81 | public void DistributedFileStoreCacheSet() 82 | { 83 | //SETUP 84 | _distributedCache.ClearAll(); 85 | 86 | //ATTEMPT 87 | _distributedCache.Set("test", "goodbye"); 88 | 89 | //VERIFY 90 | var allValues = _distributedCache.GetAllKeyValues(); 91 | allValues.Count.ShouldEqual(1); 92 | allValues["test"].ShouldEqual("goodbye"); 93 | 94 | _options.DisplayCacheFile(_output); 95 | } 96 | 97 | [Fact] 98 | public void DistributedFileStoreCacheSet_Unicode() 99 | { 100 | //SETUP 101 | _distributedCache.ClearAll(); 102 | 103 | //ATTEMPT 104 | var unicode = "בָּרוּךְ אַתָּה ה' אֱ-לֹהֵינוּ, מֶלֶך הָעוֹלָם"; 105 | _distributedCache.Set("Unicode", unicode); 106 | var nonChars = new string(new[] { (char)1, (char)2, (char)3 }); 107 | _distributedCache.Set("non-chars", nonChars); 108 | _distributedCache.Set("ascii", "my ascii"); 109 | 110 | 111 | //VERIFY 112 | var allValues = _distributedCache.GetAllKeyValues(); 113 | allValues.Count.ShouldEqual(3); 114 | allValues["Unicode"].ShouldEqual(unicode); 115 | allValues["non-chars"].ShouldEqual(nonChars); 116 | allValues["ascii"].ShouldEqual("my ascii"); 117 | 118 | _options.DisplayCacheFile(_output); 119 | } 120 | 121 | [Fact] 122 | public void DistributedFileStoreCacheSet_AbsoluteExpirationStillValid() 123 | { 124 | //SETUP 125 | _distributedCache.ClearAll(); 126 | 127 | //ATTEMPT 128 | _distributedCache.Set("test-timeout1Sec", "time1", new DistributedCacheEntryOptions{ AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(1)}); 129 | 130 | //VERIFY 131 | _distributedCache.Get("test-timeout1Sec").ShouldEqual("time1"); 132 | StaticCachePart.CacheContent.TimeOuts["test-timeout1Sec"].ShouldNotBeNull(); 133 | 134 | _options.DisplayCacheFile(_output); 135 | } 136 | 137 | [Fact] 138 | public void DistributedFileStoreCacheSet_AbsoluteExpirationExpired() 139 | { 140 | //SETUP 141 | _distributedCache.ClearAll(); 142 | 143 | //ATTEMPT 144 | _distributedCache.Set("test-timeoutExpired", "time1", new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromTicks(1) }); 145 | 146 | //VERIFY 147 | _distributedCache.Get("test-timeoutExpired").ShouldBeNull(); 148 | StaticCachePart.CacheContent.TimeOuts.ContainsKey("test-timeout1Sec").ShouldBeFalse(); 149 | 150 | _options.DisplayCacheFile(_output); 151 | } 152 | 153 | [Fact] 154 | public void DistributedFileStoreCacheSet_SlidingExpiration() 155 | { 156 | //SETUP 157 | 158 | //ATTEMPT 159 | var ex = Assert.Throws(() => _distributedCache.Set("test-bad", "time1", 160 | new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromTicks(1) })); 161 | 162 | //VERIFY 163 | ex.Message.ShouldEqual("This library doesn't support sliding expirations for performance reasons."); 164 | } 165 | 166 | [Fact] 167 | public void DistributedFileStoreCacheSetNullBad() 168 | { 169 | //SETUP 170 | _distributedCache.ClearAll(); 171 | 172 | //ATTEMPT 173 | try 174 | { 175 | _distributedCache.Set("test", null); 176 | } 177 | catch (ArgumentNullException) 178 | { 179 | return; 180 | } 181 | 182 | //VERIFY 183 | Assert.True(false, "should have throw exception"); 184 | } 185 | 186 | [Fact] 187 | public void DistributedFileStoreCacheWithSetChange() 188 | { 189 | //SETUP 190 | _distributedCache.ClearAll(); 191 | 192 | //ATTEMPT 193 | _distributedCache.Set("test", "first"); 194 | _options.DisplayCacheFile(_output); 195 | _distributedCache.Set("test", "second"); 196 | _options.DisplayCacheFile(_output); 197 | 198 | //VERIFY 199 | _output.WriteLine("------------------------------"); 200 | _output.WriteLine(string.Join(", ", _distributedCache.Get("test")!.Select(x => (int)x))); 201 | var value = _distributedCache.Get("test"); 202 | value.ShouldEqual("second"); 203 | var allValues = _distributedCache.GetAllKeyValues(); 204 | allValues.Count.ShouldEqual(1); 205 | } 206 | 207 | [Fact] 208 | public void DistributedFileStoreCacheRemove() 209 | { 210 | //SETUP 211 | _distributedCache.ClearAll(); 212 | _distributedCache.Set("XXX", "gone in a minute"); 213 | _distributedCache.Set("Still there", "keep this"); 214 | 215 | //ATTEMPT 216 | _distributedCache.Remove("XXX"); 217 | 218 | //VERIFY 219 | var allValues = _distributedCache.GetAllKeyValues(); 220 | _distributedCache.Get("XXX").ShouldBeNull(); 221 | _distributedCache.Get("Still there").ShouldEqual("keep this"); 222 | _distributedCache.GetAllKeyValues().Count.ShouldEqual(1); 223 | 224 | _options.DisplayCacheFile(_output); 225 | } 226 | 227 | [Fact] 228 | public void DistributedFileStoreCacheSetTwice() 229 | { 230 | //SETUP 231 | _distributedCache.ClearAll(); 232 | 233 | //ATTEMPT 234 | _distributedCache.Set("test1", "first"); 235 | _distributedCache.Set("test2", "second"); 236 | 237 | //VERIFY 238 | var allValues = _distributedCache.GetAllKeyValues(); 239 | allValues.Count.ShouldEqual(2); 240 | allValues["test1"].ShouldEqual("first"); 241 | allValues["test2"].ShouldEqual("second"); 242 | 243 | _options.DisplayCacheFile(_output); 244 | } 245 | 246 | [Fact] 247 | public void DistributedFileStoreCacheSetMany() 248 | { 249 | //SETUP 250 | _distributedCache.ClearAll(); 251 | 252 | //ATTEMPT 253 | _distributedCache.SetMany(new List> 254 | { 255 | new ("test1", "first"), 256 | new ("test2", "second") 257 | }); 258 | 259 | //VERIFY 260 | var allValues = _distributedCache.GetAllKeyValues(); 261 | allValues.Count.ShouldEqual(2); 262 | allValues["test1"].ShouldEqual("first"); 263 | allValues["test2"].ShouldEqual("second"); 264 | 265 | _options.DisplayCacheFile(_output); 266 | } 267 | 268 | [Fact] 269 | public void DistributedFileStoreCacheSetMany_AbsoluteExpirationRelativeToNow() 270 | { 271 | //SETUP 272 | _distributedCache.ClearAll(); 273 | 274 | //ATTEMPT 275 | _distributedCache.SetMany(new List> 276 | { 277 | new ("Timeout1", "first"), 278 | new ("Timeout2", "second") 279 | }, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromTicks(1) }); 280 | _distributedCache.Set("NotTimedOut", "I'm still here"); 281 | 282 | //VERIFY 283 | var allValues = _distributedCache.GetAllKeyValues(); 284 | allValues.Count.ShouldEqual(1); 285 | allValues["NotTimedOut"].ShouldEqual("I'm still here"); 286 | StaticCachePart.CacheContent.TimeOuts.ContainsKey("Timeout1").ShouldBeFalse(); 287 | 288 | _options.DisplayCacheFile(_output); 289 | } 290 | 291 | [Fact] 292 | public void DistributedFileStoreCacheHeavyUsage() 293 | { 294 | //SETUP 295 | 296 | 297 | //ATTEMPT 298 | for (int i = 0; i < 10; i++) 299 | { 300 | _distributedCache.ClearAll(); 301 | _distributedCache.Set($"test{i}", i.ToString()); 302 | _distributedCache.Get($"test{i}").ShouldEqual(i.ToString()); 303 | } 304 | 305 | //VERIFY 306 | var allValues = _distributedCache.GetAllKeyValues(); 307 | allValues.Count.ShouldEqual(1); 308 | 309 | _options.DisplayCacheFile(_output); 310 | } 311 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestDistributedFileStoreCacheString_Async.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.Caching.Distributed; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Net.DistributedFileStoreCache; 7 | using Net.DistributedFileStoreCache.SupportCode; 8 | using Test.TestHelpers; 9 | using TestSupport.Helpers; 10 | using Xunit; 11 | using Xunit.Abstractions; 12 | using Xunit.Extensions.AssertExtensions; 13 | 14 | namespace Test.UnitTests; 15 | 16 | // see https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel 17 | [Collection("Sequential")] 18 | public class TestDistributedFileStoreCacheString_Async 19 | { 20 | private readonly IDistributedFileStoreCacheString _distributedCache; 21 | private readonly DistributedFileStoreCacheOptions _options; 22 | private readonly ITestOutputHelper _output; 23 | 24 | public TestDistributedFileStoreCacheString_Async(ITestOutputHelper output) 25 | { 26 | _output = output; 27 | 28 | var services = new ServiceCollection(); 29 | _options = services.AddDistributedFileStoreCache(options => 30 | { 31 | options.WhichVersion = FileStoreCacheVersions.String; 32 | options.PathToCacheFileDirectory = TestData.GetTestDataDir(); 33 | options.SecondPartOfCacheFileName = GetType().Name; 34 | options.TurnOffStaticFilePathCheck = true; 35 | }); 36 | var serviceProvider = services.BuildServiceProvider(); 37 | 38 | _distributedCache = serviceProvider.GetRequiredService(); 39 | } 40 | 41 | [Fact] 42 | public async Task DistributedFileStoreCacheSetAsync() 43 | { 44 | //SETUP 45 | _distributedCache.ClearAll(); 46 | 47 | //ATTEMPT 48 | await _distributedCache.SetAsync("test", "hello async"); 49 | 50 | //VERIFY 51 | var value = await _distributedCache.GetAsync("test"); 52 | value.ShouldEqual("hello async"); 53 | 54 | var allValues = await _distributedCache.GetAllKeyValuesAsync(); 55 | allValues.Count.ShouldEqual(1); 56 | allValues["test"].ShouldEqual("hello async"); 57 | 58 | _options.DisplayCacheFile(_output); 59 | } 60 | 61 | [Fact] 62 | public async Task DistributedFileStoreCacheSet_AbsoluteExpirationStillValid() 63 | { 64 | //SETUP 65 | _distributedCache.ClearAll(); 66 | 67 | //ATTEMPT 68 | await _distributedCache.SetAsync("test-timeout1Sec", "time1", new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(1) }); 69 | 70 | //VERIFY 71 | (await _distributedCache.GetAsync("test-timeout1Sec")).ShouldEqual("time1"); 72 | StaticCachePart.CacheContent.TimeOuts["test-timeout1Sec"].ShouldNotBeNull(); 73 | 74 | _options.DisplayCacheFile(_output); 75 | } 76 | 77 | [Fact] 78 | public async Task DistributedFileStoreCacheSet_AbsoluteExpirationExpired() 79 | { 80 | //SETUP 81 | _distributedCache.ClearAll(); 82 | 83 | //ATTEMPT 84 | await _distributedCache.SetAsync("test-timeoutExpired", "time1", new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromTicks(1) }); 85 | 86 | //VERIFY 87 | (await _distributedCache.GetAsync("test-timeoutExpired")).ShouldBeNull(); 88 | StaticCachePart.CacheContent.TimeOuts.ContainsKey("test-timeout1Sec").ShouldBeFalse(); 89 | 90 | _options.DisplayCacheFile(_output); 91 | } 92 | 93 | [Fact] 94 | public async Task DistributedFileStoreCacheSet_SlidingExpiration() 95 | { 96 | //SETUP 97 | 98 | //ATTEMPT 99 | var ex = await Assert.ThrowsAsync( async () => await _distributedCache.SetAsync("test-bad", "time1", 100 | new DistributedCacheEntryOptions { SlidingExpiration = TimeSpan.FromTicks(1) })); 101 | 102 | //VERIFY 103 | ex.Message.ShouldEqual("This library doesn't support sliding expirations for performance reasons."); 104 | } 105 | 106 | [Fact] 107 | public async Task DistributedFileStoreCacheSetNullBad() 108 | { 109 | //SETUP 110 | _distributedCache.ClearAll(); 111 | 112 | //ATTEMPT 113 | try 114 | { 115 | await _distributedCache.SetAsync("test", null); 116 | } 117 | catch (ArgumentNullException) 118 | { 119 | return; 120 | } 121 | 122 | //VERIFY 123 | Assert.True(false, "should have throw exception"); 124 | } 125 | 126 | [Fact] 127 | public async Task DistributedFileStoreCacheWithSetChangeAsync() 128 | { 129 | //SETUP 130 | _distributedCache.ClearAll(); 131 | 132 | //ATTEMPT 133 | await _distributedCache.SetAsync("test", "first"); 134 | _options.DisplayCacheFile(_output); 135 | await _distributedCache.SetAsync("test", "second"); 136 | _options.DisplayCacheFile(_output); 137 | 138 | //VERIFY 139 | var value = await _distributedCache.GetAsync("test"); 140 | value.ShouldEqual("second"); 141 | var allValues = await _distributedCache.GetAllKeyValuesAsync(); 142 | allValues.Count.ShouldEqual(1); 143 | } 144 | 145 | [Fact] 146 | public async Task DistributedFileStoreCacheRemoveAsync() 147 | { 148 | //SETUP 149 | _distributedCache.ClearAll(); 150 | await _distributedCache.SetAsync("YYY", "another to go"); 151 | await _distributedCache.SetAsync("Still there", "keep this"); 152 | 153 | //ATTEMPT 154 | await _distributedCache.RemoveAsync("YYY"); 155 | 156 | //VERIFY 157 | (await _distributedCache.GetAsync("YYY")).ShouldBeNull(); 158 | (await _distributedCache.GetAllKeyValuesAsync()).Count.ShouldEqual(1); 159 | 160 | _options.DisplayCacheFile(_output); 161 | } 162 | 163 | [Fact] 164 | public async Task DistributedFileStoreCacheSetTwice() 165 | { 166 | //SETUP 167 | _distributedCache.ClearAll(); 168 | 169 | //ATTEMPT 170 | await _distributedCache.SetManyAsync(new List> 171 | { 172 | new ("test1", "first"), 173 | new ("test2", "second") 174 | }); 175 | 176 | //VERIFY 177 | var allValues = await _distributedCache.GetAllKeyValuesAsync(); 178 | allValues.Count.ShouldEqual(2); 179 | allValues["test1"].ShouldEqual("first"); 180 | allValues["test2"].ShouldEqual("second"); 181 | 182 | _options.DisplayCacheFile(_output); 183 | } 184 | 185 | [Fact] 186 | public async Task DistributedFileStoreCacheSetMany() 187 | { 188 | //SETUP 189 | _distributedCache.ClearAll(); 190 | 191 | //ATTEMPT 192 | await _distributedCache.SetManyAsync(new List> 193 | { 194 | new ("test1", "first"), 195 | new ("test2", "second") 196 | }); 197 | 198 | //VERIFY 199 | var allValues = await _distributedCache.GetAllKeyValuesAsync(); 200 | allValues.Count.ShouldEqual(2); 201 | allValues["test1"].ShouldEqual("first"); 202 | allValues["test2"].ShouldEqual("second"); 203 | 204 | _options.DisplayCacheFile(_output); 205 | } 206 | 207 | [Fact] 208 | public async Task DistributedFileStoreCacheSetMany_AbsoluteExpirationRelativeToNow() 209 | { 210 | //SETUP 211 | _distributedCache.ClearAll(); 212 | 213 | //ATTEMPT 214 | await _distributedCache.SetManyAsync(new List> 215 | { 216 | new ("Timeout1", "first"), 217 | new ("Timeout2", "second") 218 | }, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromTicks(1) }); 219 | await _distributedCache.SetAsync("NotTimedOut", "I'm still here"); 220 | 221 | //VERIFY 222 | var allValues = await _distributedCache.GetAllKeyValuesAsync(); 223 | allValues.Count.ShouldEqual(1); 224 | allValues["NotTimedOut"].ShouldEqual("I'm still here"); 225 | StaticCachePart.CacheContent.TimeOuts.ContainsKey("Timeout1").ShouldBeFalse(); 226 | 227 | _options.DisplayCacheFile(_output); 228 | } 229 | 230 | [Fact] 231 | public async Task DistributedFileStoreCacheHeavyUsage() 232 | { 233 | //SETUP 234 | 235 | 236 | //ATTEMPT 237 | for (int i = 0; i < 10; i++) 238 | { 239 | _distributedCache.ClearAll(); 240 | await _distributedCache.SetAsync($"test{i}", i.ToString()); 241 | _distributedCache.Get($"test{i}").ShouldEqual(i.ToString()); 242 | } 243 | 244 | 245 | //VERIFY 246 | var allValues = await _distributedCache.GetAllKeyValuesAsync(); 247 | allValues.Count.ShouldEqual(1); 248 | 249 | _options.DisplayCacheFile(_output); 250 | } 251 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestFileLock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text; 5 | using Net.DistributedFileStoreCache; 6 | using Net.DistributedFileStoreCache.SupportCode; 7 | using TestSupport.EfHelpers; 8 | using TestSupport.Helpers; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | using Xunit.Extensions.AssertExtensions; 12 | 13 | namespace Test.UnitTests; 14 | 15 | // see https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel 16 | [Collection("Sequential")] 17 | public class TestFileLock 18 | { 19 | private readonly string _jsonFilePath; 20 | private readonly ITestOutputHelper _output; 21 | 22 | /// Initializes a new instance of the class. 23 | public TestFileLock(ITestOutputHelper output) 24 | { 25 | _output = output; 26 | _jsonFilePath = Path.Combine(TestData.GetTestDataDir(), "testlock.json"); 27 | if (!Directory.Exists(_jsonFilePath)) 28 | File.WriteAllText(_jsonFilePath, "{\r\n \"Cache\": {}\r\n}"); 29 | 30 | } 31 | 32 | private static (byte[] bytes, int numBytes) ReadFileWithShareNone(string filePath, Action? doInLock = null) 33 | { 34 | byte[] buffer = new byte[64000]; 35 | int numBytesRead; 36 | using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, bufferSize: 1, true); 37 | { 38 | numBytesRead = fs.Read(buffer); 39 | doInLock?.Invoke(); 40 | } 41 | return (buffer, numBytesRead); 42 | } 43 | 44 | private static void CreateNewCacheFile(string filePath) 45 | { 46 | byte[] buffer = Encoding.UTF8.GetBytes("{\r\n \"Cache\": {}\r\n}"); 47 | using FileStream fs = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, bufferSize: 1, true); 48 | { 49 | fs.Write(buffer); 50 | } 51 | } 52 | 53 | 54 | [Fact] 55 | public void TestReadFileWithShareNone() 56 | { 57 | //SETUP 58 | 59 | //ATTEMPT 60 | var data = ReadFileWithShareNone(_jsonFilePath); 61 | 62 | //VERIFY 63 | var json = Encoding.UTF8.GetString(data.bytes, 0, data.numBytes); 64 | _output.WriteLine(json); 65 | } 66 | 67 | [Fact] 68 | public void TestTestReadFileWithShareNone_TryAgainOnUnauthorizedAccess() 69 | { 70 | //SETUP 71 | var options = new DistributedFileStoreCacheOptions 72 | { 73 | NumTriesOnUnauthorizedAccess = 10, 74 | DelayMillisecondsOnUnauthorizedAccess = 100 75 | }; 76 | 77 | double totalMilliseconds = 0; 78 | 79 | //ATTEMPT 80 | using (new TimeThings(x => totalMilliseconds = x.TotalTimeMilliseconds)) 81 | { 82 | var ex = Assert.Throws(() => options.TryAgainOnUnauthorizedAccess( () => 83 | ReadFileWithShareNone(_jsonFilePath, 84 | () => { ReadFileWithShareNone(_jsonFilePath, null); }))); 85 | } 86 | 87 | //VERIFY 88 | totalMilliseconds.ShouldBeInRange(10*100, 10 * 100 + 2000); 89 | } 90 | 91 | [Fact] 92 | public void TestTestReadFileWithShareNone_AccessWithinLock() 93 | { 94 | //SETUP 95 | 96 | //ATTEMPT 97 | var ex = Assert.Throws(() => 98 | ReadFileWithShareNone(_jsonFilePath, 99 | () => { ReadFileWithShareNone(_jsonFilePath, null); })); 100 | 101 | //VERIFY 102 | ex.Message.ShouldStartWith("The process cannot access the file"); 103 | ex.Message.ShouldEndWith("because it is being used by another process."); 104 | } 105 | 106 | [Fact] 107 | public void TestCreateANewFileWhenFileAlreadyExists() 108 | { 109 | //SETUP 110 | 111 | //ATTEMPT 112 | var ex = Assert.Throws(() => CreateNewCacheFile(_jsonFilePath)); 113 | 114 | //VERIFY 115 | ex.Message.ShouldEndWith("already exists."); 116 | } 117 | 118 | [Fact] 119 | public void TestTestReadFileWithShareNone_FileSystemWatcher_Changed() 120 | { 121 | //SETUP 122 | File.WriteAllText(_jsonFilePath, "{\r\n \"Cache\": {}\r\n}"); 123 | var watcher = new FileSystemWatcher(TestData.GetTestDataDir()); 124 | watcher.EnableRaisingEvents = true; 125 | watcher.NotifyFilter = NotifyFilters.LastWrite; 126 | 127 | bool hasChanged = false; 128 | watcher.Changed += (sender, args) => 129 | hasChanged = true; 130 | 131 | //ATTEMPT 132 | hasChanged.ShouldBeFalse(); 133 | using (new TimeThings(_output)) 134 | { 135 | File.WriteAllText(_jsonFilePath, "{\r\n \"Cache\": {\"Still there\": \"keep this\"}\r\n}"); 136 | } 137 | var time2 = DateTime.Now; 138 | 139 | //VERIFY 140 | var fileTime = File.GetLastWriteTime(_jsonFilePath); 141 | _output.WriteLine(fileTime.ToString("O")); 142 | hasChanged.ShouldEqual(true); 143 | } 144 | 145 | [Fact] 146 | public void TestTestReadFileWithShareNone_FileSystemWatcher_Renamed() 147 | { 148 | //SETUP 149 | var filePath = Path.Combine(TestData.GetTestDataDir(), "testlock.1.json"); 150 | File.WriteAllText(filePath, "{\r\n \"Cache\": {}\r\n}"); 151 | var watcher = new FileSystemWatcher(TestData.GetTestDataDir()); 152 | watcher.EnableRaisingEvents = true; 153 | watcher.NotifyFilter = NotifyFilters.FileName; 154 | 155 | bool hasChanged = false; 156 | var newName = Path.GetFileName(filePath); 157 | watcher.Renamed += (sender, args) => 158 | { 159 | hasChanged = true; 160 | newName = args.Name; 161 | }; 162 | 163 | //ATTEMPT 164 | hasChanged.ShouldBeFalse(); 165 | using (new TimeThings(_output)) 166 | { 167 | File.Move(filePath, Path.Combine(TestData.GetTestDataDir(), "testlock.2.json"),true); 168 | } 169 | using (new TimeThings(_output)) 170 | { 171 | File.Delete(filePath); 172 | } 173 | var time2 = DateTime.Now; 174 | 175 | //VERIFY 176 | hasChanged.ShouldEqual(true); 177 | newName.ShouldEqual("testlock.2.json"); 178 | } 179 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestIDistributedFileStoreCacheBytes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Net.DistributedFileStoreCache; 7 | using Test.TestHelpers; 8 | using TestSupport.Helpers; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | using Xunit.Extensions.AssertExtensions; 12 | 13 | namespace Test.UnitTests; 14 | 15 | // see https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel 16 | [Collection("Sequential")] 17 | public class TestIDistributedFileStoreCacheBytes 18 | { 19 | private readonly IDistributedFileStoreCacheBytes _distributedCache; 20 | private readonly DistributedFileStoreCacheOptions _options; 21 | private readonly ITestOutputHelper _output; 22 | 23 | public TestIDistributedFileStoreCacheBytes(ITestOutputHelper output) 24 | { 25 | _output = output; 26 | 27 | var services = new ServiceCollection(); 28 | _options = services.AddDistributedFileStoreCache(options => 29 | { 30 | options.WhichVersion = FileStoreCacheVersions.Bytes; 31 | options.PathToCacheFileDirectory = TestData.GetTestDataDir(); 32 | options.SecondPartOfCacheFileName = GetType().Name; 33 | options.TurnOffStaticFilePathCheck = true; 34 | }); 35 | var serviceProvider = services.BuildServiceProvider(); 36 | 37 | _distributedCache = serviceProvider.GetRequiredService(); 38 | } 39 | 40 | [Fact] 41 | public void DistributedFileStoreCacheEmpty() 42 | { 43 | //SETUP 44 | _distributedCache.ClearAll(); 45 | 46 | //ATTEMPT 47 | var value = _distributedCache.Get("test"); 48 | 49 | //VERIFY 50 | value.ShouldBeNull(); 51 | _distributedCache.GetAllKeyValues().Count.ShouldEqual(0); 52 | 53 | _options.DisplayCacheFile(_output); 54 | } 55 | 56 | [Fact] 57 | public void DistributedFileStoreCacheSet() 58 | { 59 | //SETUP 60 | _distributedCache.ClearAll(); 61 | 62 | //ATTEMPT 63 | _distributedCache.Set("test", new byte[] { 1, 2, 3 }, null); 64 | 65 | //VERIFY 66 | var allValues = _distributedCache.GetAllKeyValues(); 67 | allValues.Count.ShouldEqual(1); 68 | allValues["test"].ShouldEqual(new byte[] { 1, 2, 3 }); 69 | 70 | _options.DisplayCacheFile(_output); 71 | } 72 | 73 | [Fact] 74 | public void DistributedFileStoreCacheSetAscci() 75 | { 76 | //SETUP 77 | _distributedCache.ClearAll(); 78 | 79 | //ATTEMPT 80 | var byteAscii = Encoding.ASCII.GetBytes("123"); 81 | _distributedCache.Set("test", byteAscii, null); 82 | 83 | //VERIFY 84 | var allValues = _distributedCache.GetAllKeyValues(); 85 | allValues.Count.ShouldEqual(1); 86 | allValues["test"].ShouldEqual(byteAscii); 87 | 88 | _options.DisplayCacheFile(_output); 89 | } 90 | 91 | [Fact] 92 | public async Task DistributedFileStoreCacheSetAsync() 93 | { 94 | //SETUP 95 | _distributedCache.ClearAll(); 96 | 97 | //ATTEMPT 98 | await _distributedCache.SetAsync("test", new byte[] { 1, 2, 3 }, null); 99 | 100 | //VERIFY 101 | var allValues = _distributedCache.GetAllKeyValues(); 102 | allValues.Count.ShouldEqual(1); 103 | allValues["test"].ShouldEqual(new byte[] { 1, 2, 3 }); 104 | 105 | _options.DisplayCacheFile(_output); 106 | } 107 | 108 | [Fact] 109 | public void DistributedFileStoreCacheSetNullBad() 110 | { 111 | //SETUP 112 | _distributedCache.ClearAll(); 113 | 114 | //ATTEMPT 115 | try 116 | { 117 | _distributedCache.Set("test", null, null); 118 | } 119 | catch (ArgumentNullException) 120 | { 121 | return; 122 | } 123 | 124 | //VERIFY 125 | Assert.True(false, "should have throw exception"); 126 | } 127 | 128 | [Fact] 129 | public void DistributedFileStoreCacheWithSetChange() 130 | { 131 | //SETUP 132 | _distributedCache.ClearAll(); 133 | 134 | //ATTEMPT 135 | _distributedCache.Set("test", new byte[] { 7, 8, 9 }, null); 136 | _distributedCache.Set("test", new byte[] { 9, 8, 7 }, null); 137 | 138 | //VERIFY 139 | var allValues = _distributedCache.GetAllKeyValues(); 140 | allValues.Count.ShouldEqual(1); 141 | allValues["test"].ShouldEqual(new byte[] { 9, 8, 7 }); 142 | 143 | _options.DisplayCacheFile(_output); 144 | } 145 | 146 | [Fact] 147 | public void DistributedFileStoreCacheRemove() 148 | { 149 | //SETUP 150 | _distributedCache.ClearAll(); 151 | _distributedCache.Set("test", new byte[] { 11, 12, 13 }, null); 152 | 153 | //ATTEMPT 154 | _distributedCache.Remove("test"); 155 | 156 | //VERIFY 157 | _distributedCache.GetAllKeyValues().Count.ShouldEqual(0); 158 | 159 | _options.DisplayCacheFile(_output); 160 | } 161 | 162 | [Fact] 163 | public async Task DistributedFileStoreCacheRemoveAsync() 164 | { 165 | //SETUP 166 | _distributedCache.ClearAll(); 167 | await _distributedCache.SetAsync("test", new byte[] { 11, 12, 13 }, null); 168 | 169 | //ATTEMPT 170 | await _distributedCache.RemoveAsync("test"); 171 | 172 | //VERIFY 173 | _distributedCache.GetAllKeyValues().Count.ShouldEqual(0); 174 | 175 | _options.DisplayCacheFile(_output); 176 | } 177 | 178 | [Fact] 179 | public void DistributedFileStoreCacheSetTwice() 180 | { 181 | //SETUP 182 | _distributedCache.ClearAll(); 183 | 184 | //ATTEMPT 185 | _distributedCache.Set("test1", new byte[] { 1, 2, 3 }, null); 186 | _distributedCache.Set("test2", new byte[] { 4, 5, 6 }, null); 187 | 188 | //VERIFY 189 | var allValues = _distributedCache.GetAllKeyValues(); 190 | allValues.Count.ShouldEqual(2); 191 | allValues["test1"].ShouldEqual(new byte[] { 1, 2, 3 }); 192 | allValues["test2"].ShouldEqual(new byte[] { 4, 5, 6 }); 193 | 194 | _options.DisplayCacheFile(_output); 195 | } 196 | 197 | [Fact] 198 | public void DistributedFileStoreCacheSet_Refresh() 199 | { 200 | //SETUP 201 | 202 | //ATTEMPT 203 | var ex = Assert.Throws(() => _distributedCache.Refresh("test")); 204 | 205 | //VERIFY 206 | ex.Message.ShouldEqual("This library doesn't support sliding expirations for performance reasons."); 207 | } 208 | 209 | [Fact] 210 | public async Task DistributedFileStoreCacheSet_RefreshAsync() 211 | { 212 | //SETUP 213 | 214 | //ATTEMPT 215 | var ex = await Assert.ThrowsAsync( async () => await _distributedCache.RefreshAsync("test")); 216 | 217 | //VERIFY 218 | ex.Message.ShouldEqual("This library doesn't support sliding expirations for performance reasons."); 219 | } 220 | 221 | 222 | 223 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestJsonSerializerOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text.Encodings.Web; 5 | using System.Text.Json; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Net.DistributedFileStoreCache; 8 | using Net.DistributedFileStoreCache.SupportCode; 9 | using TestSupport.Helpers; 10 | using Xunit; 11 | using Xunit.Abstractions; 12 | using Xunit.Extensions.AssertExtensions; 13 | 14 | namespace Test.UnitTests; 15 | 16 | // see https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel 17 | [Collection("Sequential")] 18 | public class TestJsonSerializerOptions 19 | { 20 | private readonly ITestOutputHelper _output; 21 | private DistributedFileStoreCacheOptions _options; 22 | 23 | public TestJsonSerializerOptions(ITestOutputHelper output) 24 | { 25 | _output = output; 26 | } 27 | 28 | private IDistributedFileStoreCacheString SetupCache(JsonSerializerOptions jsonOptions) 29 | { 30 | var services = new ServiceCollection(); 31 | _options = services.AddDistributedFileStoreCache(options => 32 | { 33 | options.WhichVersion = FileStoreCacheVersions.String; 34 | options.PathToCacheFileDirectory = TestData.GetTestDataDir(); 35 | options.SecondPartOfCacheFileName = GetType().Name; 36 | options.TurnOffStaticFilePathCheck = true; 37 | 38 | options.JsonSerializerForCacheFile = jsonOptions; 39 | }); 40 | var serviceProvider = services.BuildServiceProvider(); 41 | 42 | return serviceProvider.GetRequiredService(); 43 | } 44 | 45 | [Fact] 46 | public void TestDefaultJsonSerializer() 47 | { 48 | //SETUP 49 | var cache = SetupCache(new JsonSerializerOptions()); 50 | cache.ClearAll(); 51 | 52 | //ATTEMPT 53 | cache.Set("Test", "Hello today!", null); 54 | 55 | //VERIFY 56 | var fileContent = File.ReadAllText(_options.FormCacheFilePath()); 57 | fileContent.ShouldEqual(@"{""Cache"":{""Test"":""Hello today!""},""TimeOuts"":{}}"); 58 | } 59 | 60 | [Fact] 61 | public void TestJsonSerializerWriteIndented() 62 | { 63 | //SETUP 64 | var cache = SetupCache(new JsonSerializerOptions { WriteIndented = true}); 65 | cache.ClearAll(); 66 | 67 | //ATTEMPT 68 | cache.Set("Test", "Hello today!", null); 69 | 70 | //VERIFY 71 | var fileContent = File.ReadAllText(_options.FormCacheFilePath()); 72 | fileContent.ShouldEqual(@"{ 73 | ""Cache"": { 74 | ""Test"": ""Hello today!"" 75 | }, 76 | ""TimeOuts"": {} 77 | }"); 78 | } 79 | 80 | [Fact] 81 | public void TestDefaultJsonSerializer_JsonInJson() 82 | { 83 | //SETUP 84 | var cache = SetupCache(new JsonSerializerOptions()); 85 | cache.ClearAll(); 86 | 87 | var value = JsonSerializer.Serialize(new Dictionary 88 | { 89 | {1, "One"}, {2,"Two"} 90 | }); 91 | 92 | //ATTEMPT 93 | cache.Set("Json", value, null); 94 | 95 | //VERIFY 96 | var fileContent = File.ReadAllText(_options.FormCacheFilePath()); 97 | fileContent.ShouldEqual(@"{""Cache"":{""Json"":""{\u00221\u0022:\u0022One\u0022,\u00222\u0022:\u0022Two\u0022}""},""TimeOuts"":{}}"); 98 | } 99 | 100 | [Fact] 101 | public void TestJsonSerializerUnsafeRelaxedJsonEscaping_JsonInJson() 102 | { 103 | //SETUP 104 | var cache = SetupCache(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); 105 | cache.ClearAll(); 106 | 107 | var value = JsonSerializer.Serialize(new Dictionary 108 | { 109 | {1, "One"}, {2,"Two"} 110 | }); 111 | 112 | //ATTEMPT 113 | cache.Set("Json", value, null); 114 | 115 | //VERIFY 116 | var fileContent = File.ReadAllText(_options.FormCacheFilePath()); 117 | fileContent.ShouldEqual(@"{""Cache"":{""Json"":""{\""1\"":\""One\"",\""2\"":\""Two\""}""},""TimeOuts"":{}}"); 118 | } 119 | 120 | //This test shows that UnsafeRelaxedJsonEscaping doesn't do anything different to normal 121 | [Fact] 122 | public void TestJsonSerializerUnsafeRelaxedJsonEscaping_ASCII() 123 | { 124 | //SETUP 125 | var cache = SetupCache(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); 126 | cache.ClearAll(); 127 | 128 | //ATTEMPT 129 | cache.Set("Test", "Hello today!", null); 130 | 131 | //VERIFY 132 | var fileContent = File.ReadAllText(_options.FormCacheFilePath()); 133 | fileContent.ShouldEqual(@"{""Cache"":{""Test"":""Hello today!""},""TimeOuts"":{}}"); 134 | } 135 | 136 | //This test shows that UnsafeRelaxedJsonEscaping doesn't do anything different to normal 137 | [Fact] 138 | public void TestJsonSerializerUnsafeRelaxedJsonEscaping_Bytes() 139 | { 140 | //SETUP 141 | var cache = SetupCache(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); 142 | cache.ClearAll(); 143 | 144 | var value = new string(new[] { (char)1, (char)2, (char)3 }); 145 | 146 | //ATTEMPT 147 | cache.Set("Test", value, null); 148 | 149 | //VERIFY 150 | var fileContent = File.ReadAllText(_options.FormCacheFilePath()); 151 | fileContent.ShouldEqual(@"{""Cache"":{""Test"":""\u0001\u0002\u0003""},""TimeOuts"":{}}"); 152 | } 153 | 154 | //This test shows that UnsafeRelaxedJsonEscaping doesn't do anything different to normal 155 | [Fact] 156 | public void TestJsonSerializerUnsafeRelaxedJsonEscaping_Unicode() 157 | { 158 | //SETUP 159 | var cache = SetupCache(new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }); 160 | cache.ClearAll(); 161 | 162 | var value = "בָּרוּךְ אַתָּה ה' אֱ-לֹהֵינוּ, מֶלֶך הָעוֹלָם"; 163 | 164 | //ATTEMPT 165 | cache.Set("Test", value, null); 166 | 167 | //VERIFY 168 | var fileContent = File.ReadAllText(_options.FormCacheFilePath()); 169 | fileContent.ShouldEqual(@"{""Cache"":{""Test"":""בָּרוּךְ אַתָּה ה' אֱ-לֹהֵינוּ, מֶלֶך הָעוֹלָם""},""TimeOuts"":{}}"); 170 | } 171 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestJsonSerializerStream.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text.Json; 5 | using Net.DistributedFileStoreCache.SupportCode; 6 | using TestSupport.Helpers; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | 10 | namespace Test.UnitTests; 11 | 12 | //NOTE: I tried using streaming to improve performance, but it didn't make any change 13 | //I decided to NOT use streaming because you could add more data than the sync version can handle 14 | public class TestJsonSerializerStream 15 | { 16 | private readonly ITestOutputHelper _output; 17 | private readonly string _filePath; 18 | 19 | public TestJsonSerializerStream(ITestOutputHelper output) 20 | { 21 | _output = output; 22 | _filePath = Path.Combine(TestData.GetTestDataDir(), $"{GetType().Name}.json"); 23 | } 24 | 25 | private async Task UpdateFileInLock(string key, string value) 26 | { 27 | using FileStream fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None, bufferSize: 1, true); 28 | { 29 | var reader =new StreamReader(fileStream).BaseStream; 30 | var json = JsonSerializer.Deserialize(reader, new JsonSerializerOptions()); 31 | json.Cache[key] = value; 32 | fileStream.Seek(0, SeekOrigin.Begin); 33 | fileStream.SetLength(0); 34 | var writer = new StreamWriter(fileStream).BaseStream; 35 | 36 | await JsonSerializer.SerializeAsync(writer, json); 37 | } 38 | } 39 | 40 | [Fact] 41 | public async Task TestUpdateJsonFile() 42 | { 43 | //SETUP 44 | File.WriteAllText(_filePath, "{\r\n \"Cache\": {}\r\n}"); 45 | 46 | //ATTEMPT 47 | await UpdateFileInLock("test", "does it work"); 48 | 49 | //VERIFY 50 | _output.WriteLine(File.ReadAllText(_filePath)); 51 | } 52 | 53 | 54 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestMaxBytes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text.Encodings.Web; 5 | using System.Text.Json; 6 | using Microsoft.Extensions.Caching.Distributed; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Net.DistributedFileStoreCache; 9 | using Net.DistributedFileStoreCache.SupportCode; 10 | using TestSupport.Helpers; 11 | using Xunit; 12 | using Xunit.Abstractions; 13 | using Xunit.Extensions.AssertExtensions; 14 | 15 | namespace Test.UnitTests; 16 | 17 | // see https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel 18 | [Collection("Sequential")] 19 | public class TestMaxBytes 20 | { 21 | private readonly ITestOutputHelper _output; 22 | private DistributedFileStoreCacheOptions _options; 23 | 24 | public TestMaxBytes(ITestOutputHelper output) 25 | { 26 | _output = output; 27 | } 28 | 29 | private IDistributedFileStoreCacheString SetupCache(int maxBytes, bool jsonEscape = false) 30 | { 31 | var services = new ServiceCollection(); 32 | _options = services.AddDistributedFileStoreCache(options => 33 | { 34 | options.WhichVersion = FileStoreCacheVersions.String; 35 | options.PathToCacheFileDirectory = TestData.GetTestDataDir(); 36 | options.SecondPartOfCacheFileName = GetType().Name; 37 | options.TurnOffStaticFilePathCheck = true; 38 | 39 | if (jsonEscape) 40 | options.JsonSerializerForCacheFile = 41 | new JsonSerializerOptions { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; 42 | 43 | options.MaxBytesInJsonCacheFile = maxBytes; 44 | }); 45 | var serviceProvider = services.BuildServiceProvider(); 46 | 47 | return serviceProvider.GetRequiredService(); 48 | } 49 | 50 | [Theory] 51 | [InlineData(1)] 52 | [InlineData(2)] 53 | [InlineData(3)] 54 | [InlineData(10)] 55 | [InlineData(100)] 56 | public void TestSetMaxBytesByCalculation(int numValues) 57 | { 58 | //SETUP 59 | var tempOptions = new DistributedFileStoreCacheOptions(); 60 | tempOptions.SetMaxBytesByCalculation(numValues, 7, 30); 61 | var cache = SetupCache(tempOptions.MaxBytesInJsonCacheFile); 62 | cache.ClearAll(); 63 | 64 | //ATTEMPT 65 | for (int i = 0; i < 100; i++) 66 | { 67 | cache.Set($"Test{i:D3}", "123456789012345678901234567890", null); 68 | } 69 | 70 | //VERIFY 71 | _output.WriteLine( 72 | $"Calculated maxBytes = {_options.MaxBytesInJsonCacheFile}, Actual size = {File.ReadAllText(_options.FormCacheFilePath()).Length}"); 73 | cache.GetAllKeyValues().Count.ShouldEqual(numValues); 74 | } 75 | 76 | [Theory] 77 | [InlineData(1)] 78 | [InlineData(2)] 79 | [InlineData(3)] 80 | [InlineData(10)] 81 | [InlineData(100)] 82 | public void TestSetMaxBytesByCalculation_WithTimeout(int numValues) 83 | { 84 | //SETUP 85 | var tempOptions = new DistributedFileStoreCacheOptions(); 86 | tempOptions.SetMaxBytesByCalculation(numValues, 7, 30, 1, 100); 87 | var cache = SetupCache(tempOptions.MaxBytesInJsonCacheFile); 88 | cache.ClearAll(); 89 | 90 | //ATTEMPT 91 | for (int i = 0; i < 100; i++) 92 | { 93 | cache.Set($"Test{i:D3}", "123456789012345678901234567890", 94 | new DistributedCacheEntryOptions{AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(1) }); 95 | } 96 | 97 | //VERIFY 98 | _output.WriteLine( 99 | $"Calculated maxBytes = {_options.MaxBytesInJsonCacheFile}, Actual size = {File.ReadAllText(_options.FormCacheFilePath()).Length}"); 100 | cache.GetAllKeyValues().Count.ShouldEqual(numValues); 101 | } 102 | 103 | 104 | [Fact] 105 | public void TestSetMaxBytesByCalculation_Unicode_NoJsonEscape() 106 | { 107 | //SETUP 108 | int numValues = 5; 109 | var unicode = "בָּרוּךְ אַתָּה ה' אֱ-לֹהֵינוּ, מֶלֶך הָעוֹלָם"; 110 | var tempOptions = new DistributedFileStoreCacheOptions(); 111 | tempOptions.SetMaxBytesByCalculation(numValues, 7, unicode.Length, 6); 112 | var cache = SetupCache(tempOptions.MaxBytesInJsonCacheFile, false); 113 | cache.ClearAll(); 114 | 115 | //ATTEMPT 116 | for (int i = 0; i < 100; i++) 117 | { 118 | cache.Set($"Test{i:D3}", unicode, null); 119 | } 120 | 121 | //VERIFY 122 | _output.WriteLine( 123 | $"Calculated maxBytes = {_options.MaxBytesInJsonCacheFile}, Actual size = {File.ReadAllText(_options.FormCacheFilePath()).Length}"); 124 | cache.GetAllKeyValues().Count.ShouldEqual(numValues); 125 | } 126 | 127 | [Fact] 128 | public void TestSetMaxBytesByCalculation_Unicode_WithJsonEscape() 129 | { 130 | //SETUP 131 | int numValues = 5; 132 | var unicode = "בָּרוּךְ אַתָּה ה' אֱ-לֹהֵינוּ, מֶלֶך הָעוֹלָם"; 133 | var tempOptions = new DistributedFileStoreCacheOptions(); 134 | tempOptions.SetMaxBytesByCalculation(numValues, 7, unicode.Length, 2); 135 | var cache = SetupCache(tempOptions.MaxBytesInJsonCacheFile, true); 136 | cache.ClearAll(); 137 | 138 | //ATTEMPT 139 | for (int i = 0; i < 100; i++) 140 | { 141 | cache.Set($"Test{i:D3}", unicode, null); 142 | } 143 | 144 | //VERIFY 145 | _output.WriteLine( 146 | $"Calculated maxBytes = {_options.MaxBytesInJsonCacheFile}, Actual size = {File.ReadAllText(_options.FormCacheFilePath()).Length}"); 147 | cache.GetAllKeyValues().Count.ShouldEqual(numValues); 148 | } 149 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestSqlServerTiming.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.ComponentModel.DataAnnotations; 5 | using Dapper; 6 | using Microsoft.EntityFrameworkCore; 7 | using TestSupport.Attributes; 8 | using TestSupport.EfHelpers; 9 | using Xunit.Abstractions; 10 | using Xunit.Extensions.AssertExtensions; 11 | 12 | namespace Test.UnitTests; 13 | 14 | 15 | public class TestSqlServerTiming 16 | { 17 | private readonly ITestOutputHelper _output; 18 | 19 | public TestSqlServerTiming(ITestOutputHelper output) 20 | { 21 | _output = output; 22 | } 23 | 24 | public class MyCache 25 | { 26 | [Key] 27 | [MaxLength(100)] 28 | public string Name { get; set; } 29 | public string Value { get; set; } 30 | } 31 | 32 | 33 | 34 | public class TestDbContext : DbContext 35 | { 36 | public DbSet Cache { get; set; } 37 | 38 | public TestDbContext(DbContextOptions options) 39 | : base(options) { } 40 | } 41 | 42 | 43 | 44 | //I build this to see how quick a sql server cache could be 45 | //Remember: a Set does an create or update, while the SQL only does create part 46 | //That means that the SQL performance is better than a SQL cache library 47 | [RunnableInDebugOnly] 48 | public void TestSqlServerRaw() 49 | { 50 | //SETUP 51 | var options = this.CreateUniqueClassOptions(); 52 | var context = new TestDbContext(options); 53 | 54 | context.Database.EnsureDeleted(); 55 | context.Database.EnsureCreated(); 56 | 57 | const int NumTest = 100; 58 | //warmup 59 | for (int i = 0; i < 10; i++) 60 | { 61 | var insert = String.Format("INSERT INTO Cache (Name, Value) VALUES ('{0}', '{1}')", $"Key1{i:D4}", 62 | DateTime.Now.Ticks.ToString()); 63 | context.Database.ExecuteSqlRaw(insert); 64 | } 65 | 66 | //ATTEMPT 67 | using (new TimeThings(_output, "sql write", NumTest)) 68 | { 69 | for (int i = 0; i < NumTest; i++) 70 | { 71 | var sql = String.Format("INSERT INTO Cache (Name, Value) VALUES ('{0}', '{1}')", $"Key2{i:D4}", 72 | DateTime.Now.Ticks.ToString()); 73 | 74 | context.Database.GetDbConnection().QuerySingleOrDefault(sql); 75 | } 76 | } 77 | 78 | string read = null; 79 | using (new TimeThings(_output, "sql read", NumTest)) 80 | { 81 | for (int i = 0; i < NumTest; i++) 82 | { 83 | var sql = $"SELECT [c].[Value] FROM [Cache] AS [c] WHERE [c].[Name] = 'Key2{i:D4}'"; 84 | read = context.Database.GetDbConnection().QuerySingleOrDefault(sql); 85 | } 86 | } 87 | read.ShouldNotBeNull(); 88 | 89 | //VERIFY 90 | } 91 | } -------------------------------------------------------------------------------- /Test/UnitTests/TestStaticCachePart.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2022 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/ 2 | // Licensed under MIT license. See License.txt in the project root for license information. 3 | 4 | using System.Text; 5 | using Net.DistributedFileStoreCache; 6 | using Net.DistributedFileStoreCache.SupportCode; 7 | using Test.TestHelpers; 8 | using TestSupport.Helpers; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | using Xunit.Extensions.AssertExtensions; 12 | 13 | namespace Test.UnitTests; 14 | 15 | // see https://stackoverflow.com/questions/1408175/execute-unit-tests-serially-rather-than-in-parallel 16 | [Collection("Sequential")] 17 | public class TestStaticCachePart 18 | { 19 | private readonly DistributedFileStoreCacheOptions _options; 20 | private readonly ITestOutputHelper _output; 21 | 22 | public TestStaticCachePart(ITestOutputHelper output) 23 | { 24 | _output = output; 25 | 26 | _options = new DistributedFileStoreCacheOptions 27 | { 28 | PathToCacheFileDirectory = TestData.GetTestDataDir(), 29 | SecondPartOfCacheFileName = GetType().Name, 30 | TurnOffStaticFilePathCheck = true 31 | }; 32 | } 33 | 34 | private static void CreateNewCacheFile(string filePath) 35 | { 36 | byte[] buffer = Encoding.UTF8.GetBytes("{\r\n \"Cache\": {}\r\n}"); 37 | using FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 1, true); 38 | { 39 | fs.Write(buffer); 40 | } 41 | } 42 | 43 | [Fact] 44 | public void TestFileSystemWatcherChange_WriteAllFiles() 45 | { 46 | //SETUP 47 | var watcher = new FileSystemWatcher(_options.PathToCacheFileDirectory, _options.FormCacheFileName()); 48 | watcher.EnableRaisingEvents = true; 49 | watcher.NotifyFilter = NotifyFilters.LastWrite; 50 | 51 | var count = 0; 52 | watcher.Changed += (sender, args) => count++; 53 | 54 | //ATTEMPT 55 | File.WriteAllText(_options.FormCacheFilePath(), "{\r\n \"Cache\": {\r\n \"test\": \"goodbye\"\r\n }\r\n}"); 56 | 57 | //VERIFY 58 | count.ShouldBeInRange(1, 2); 59 | _output.WriteLine($"Triggered {count} times"); 60 | } 61 | 62 | [Fact] 63 | public void TestFileSystemWatcherChange_CreateNewCacheFile() 64 | { 65 | //SETUP 66 | var watcher = new FileSystemWatcher(_options.PathToCacheFileDirectory, _options.FormCacheFileName()); 67 | watcher.EnableRaisingEvents = true; 68 | watcher.NotifyFilter = NotifyFilters.LastWrite; 69 | 70 | var count = 0; 71 | watcher.Changed += (sender, args) => count++; 72 | 73 | //ATTEMPT 74 | CreateNewCacheFile(_options.FormCacheFilePath()); 75 | 76 | //VERIFY 77 | count.ShouldBeInRange(1, 2); 78 | _output.WriteLine($"Triggered {count} times"); 79 | } 80 | 81 | [Fact] 82 | public void TestStartupNoCacheFile() 83 | { 84 | //SETUP 85 | if (File.Exists(_options.FormCacheFilePath())) 86 | File.Delete(_options.FormCacheFilePath()); 87 | 88 | //ATTEMPT 89 | StaticCachePart.SetupStaticCache(_options); 90 | 91 | 92 | //VERIFY 93 | File.Exists(_options.FormCacheFilePath()).ShouldBeTrue(); 94 | StaticCachePart.CacheContent.Cache.ShouldEqual(new Dictionary()); 95 | StaticCachePart.CacheContent.TimeOuts.ShouldEqual(new Dictionary()); 96 | _options.DisplayCacheFile(_output); 97 | } 98 | 99 | [Fact] 100 | public void TestStartupCacheFileExists() 101 | { 102 | //SETUP 103 | File.WriteAllText(_options.FormCacheFilePath(), "{\r\n \"Cache\": {\r\n \"test\": \"goodbye\"\r\n }\r\n}"); 104 | 105 | //ATTEMPT 106 | StaticCachePart.SetupStaticCache(_options); 107 | 108 | //VERIFY 109 | _options.DisplayCacheFile(_output); 110 | } 111 | 112 | [Fact] 113 | public void TestStartupCacheChangeCacheFile() 114 | { 115 | //SETUP 116 | if (File.Exists(_options.FormCacheFilePath())) File.Delete(_options.FormCacheFilePath()); 117 | StaticCachePart.SetupStaticCache(_options); 118 | 119 | //ATTEMPT 120 | File.WriteAllText(_options.FormCacheFilePath(), "{\r\n \"Cache\": {\r\n \"test\": \"goodbye\"\r\n }\r\n}"); 121 | 122 | //VERIFY 123 | _options.DisplayCacheFile(_output); 124 | StaticCachePart.LocalCacheIsOutOfDate.ShouldEqual(true); 125 | } 126 | } -------------------------------------------------------------------------------- /Test/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "UnitTestConnection": "Server=(localdb)\\mssqllocaldb;Database=FileStoreCache-Test;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | } 5 | } 6 | --------------------------------------------------------------------------------