├── .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 |
--------------------------------------------------------------------------------