├── .gitattributes
├── .gitignore
├── LICENSE
├── MongoDbCache.sln
├── README.md
└── src
├── MongoDbCache.Tests
├── Infrastructure
│ └── MongoDbCacheConfig.cs
├── MongoDbCache.Tests.csproj
├── MongoDbCacheServiceExtensionsTests.cs
├── MongoDbCacheSetAndRemoveTests.cs
├── TestDistributedCache.cs
└── TimeExpirationTests.cs
└── MongoDbCache
├── CacheItem.cs
├── MongoContext.cs
├── MongoDbCache.cs
├── MongoDbCache.csproj
├── MongoDbCacheOptions.cs
└── MongoDbCacheServicesExtensions.cs
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | bld/
21 | [Bb]in/
22 | [Oo]bj/
23 | [Ll]og/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | project.lock.json
45 | artifacts/
46 |
47 | *_i.c
48 | *_p.c
49 | *_i.h
50 | *.ilk
51 | *.meta
52 | *.obj
53 | *.pch
54 | *.pdb
55 | *.pgc
56 | *.pgd
57 | *.rsp
58 | *.sbr
59 | *.tlb
60 | *.tli
61 | *.tlh
62 | *.tmp
63 | *.tmp_proj
64 | *.log
65 | *.vspscc
66 | *.vssscc
67 | .builds
68 | *.pidb
69 | *.svclog
70 | *.scc
71 |
72 | # Chutzpah Test files
73 | _Chutzpah*
74 |
75 | # Visual C++ cache files
76 | ipch/
77 | *.aps
78 | *.ncb
79 | *.opendb
80 | *.opensdf
81 | *.sdf
82 | *.cachefile
83 | *.VC.db
84 | *.VC.VC.opendb
85 |
86 | # Visual Studio profiler
87 | *.psess
88 | *.vsp
89 | *.vspx
90 | *.sap
91 |
92 | # TFS 2012 Local Workspace
93 | $tf/
94 |
95 | # Guidance Automation Toolkit
96 | *.gpState
97 |
98 | # ReSharper is a .NET coding add-in
99 | _ReSharper*/
100 | *.[Rr]e[Ss]harper
101 | *.DotSettings.user
102 |
103 | # JustCode is a .NET coding add-in
104 | .JustCode
105 |
106 | # TeamCity is a build add-in
107 | _TeamCity*
108 |
109 | # DotCover is a Code Coverage Tool
110 | *.dotCover
111 |
112 | # NCrunch
113 | _NCrunch_*
114 | .*crunch*.local.xml
115 | nCrunchTemp_*
116 |
117 | # MightyMoose
118 | *.mm.*
119 | AutoTest.Net/
120 |
121 | # Web workbench (sass)
122 | .sass-cache/
123 |
124 | # Installshield output folder
125 | [Ee]xpress/
126 |
127 | # DocProject is a documentation generator add-in
128 | DocProject/buildhelp/
129 | DocProject/Help/*.HxT
130 | DocProject/Help/*.HxC
131 | DocProject/Help/*.hhc
132 | DocProject/Help/*.hhk
133 | DocProject/Help/*.hhp
134 | DocProject/Help/Html2
135 | DocProject/Help/html
136 |
137 | # Click-Once directory
138 | publish/
139 |
140 | # Publish Web Output
141 | *.[Pp]ublish.xml
142 | *.azurePubxml
143 | # TODO: Comment the next line if you want to checkin your web deploy settings
144 | # but database connection strings (with potential passwords) will be unencrypted
145 | *.pubxml
146 | *.publishproj
147 |
148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
149 | # checkin your Azure Web App publish settings, but sensitive information contained
150 | # in these scripts will be unencrypted
151 | PublishScripts/
152 |
153 | # NuGet Packages
154 | *.nupkg
155 | # The packages folder can be ignored because of Package Restore
156 | **/packages/*
157 | # except build/, which is used as an MSBuild target.
158 | !**/packages/build/
159 | # Uncomment if necessary however generally it will be regenerated when needed
160 | #!**/packages/repositories.config
161 | # NuGet v3's project.json files produces more ignoreable files
162 | *.nuget.props
163 | *.nuget.targets
164 |
165 | # Microsoft Azure Build Output
166 | csx/
167 | *.build.csdef
168 |
169 | # Microsoft Azure Emulator
170 | ecf/
171 | rcf/
172 |
173 | # Windows Store app package directories and files
174 | AppPackages/
175 | BundleArtifacts/
176 | Package.StoreAssociation.xml
177 | _pkginfo.txt
178 |
179 | # Visual Studio cache files
180 | # files ending in .cache can be ignored
181 | *.[Cc]ache
182 | # but keep track of directories ending in .cache
183 | !*.[Cc]ache/
184 |
185 | # Others
186 | ClientBin/
187 | ~$*
188 | *~
189 | *.dbmdl
190 | *.dbproj.schemaview
191 | *.pfx
192 | *.publishsettings
193 | node_modules/
194 | orleans.codegen.cs
195 |
196 | # Since there are multiple workflows, uncomment next line to ignore bower_components
197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
198 | #bower_components/
199 |
200 | # RIA/Silverlight projects
201 | Generated_Code/
202 |
203 | # Backup & report files from converting an old project file
204 | # to a newer Visual Studio version. Backup files are not needed,
205 | # because we have git ;-)
206 | _UpgradeReport_Files/
207 | Backup*/
208 | UpgradeLog*.XML
209 | UpgradeLog*.htm
210 |
211 | # SQL Server files
212 | *.mdf
213 | *.ldf
214 |
215 | # Business Intelligence projects
216 | *.rdl.data
217 | *.bim.layout
218 | *.bim_*.settings
219 |
220 | # Microsoft Fakes
221 | FakesAssemblies/
222 |
223 | # GhostDoc plugin setting file
224 | *.GhostDoc.xml
225 |
226 | # Node.js Tools for Visual Studio
227 | .ntvs_analysis.dat
228 |
229 | # Visual Studio 6 build log
230 | *.plg
231 |
232 | # Visual Studio 6 workspace options file
233 | *.opt
234 |
235 | # Visual Studio LightSwitch build output
236 | **/*.HTMLClient/GeneratedArtifacts
237 | **/*.DesktopClient/GeneratedArtifacts
238 | **/*.DesktopClient/ModelManifest.xml
239 | **/*.Server/GeneratedArtifacts
240 | **/*.Server/ModelManifest.xml
241 | _Pvt_Extensions
242 |
243 | # Paket dependency manager
244 | .paket/paket.exe
245 | paket-files/
246 |
247 | # FAKE - F# Make
248 | .fake/
249 |
250 | # JetBrains Rider
251 | .idea/
252 | *.sln.iml
253 |
254 | # =========================
255 | # Operating System Files
256 | # =========================
257 |
258 | # OSX
259 | # =========================
260 |
261 | .DS_Store
262 | .AppleDouble
263 | .LSOverride
264 |
265 | # Thumbnails
266 | ._*
267 |
268 | # Files that might appear in the root of a volume
269 | .DocumentRevisions-V100
270 | .fseventsd
271 | .Spotlight-V100
272 | .TemporaryItems
273 | .Trashes
274 | .VolumeIcon.icns
275 |
276 | # Directories potentially created on remote AFP share
277 | .AppleDB
278 | .AppleDesktop
279 | Network Trash Folder
280 | Temporary Items
281 | .apdisk
282 |
283 | # Windows
284 | # =========================
285 |
286 | # Windows image file caches
287 | Thumbs.db
288 | ehthumbs.db
289 |
290 | # Folder config file
291 | Desktop.ini
292 |
293 | # Recycle Bin used on file shares
294 | $RECYCLE.BIN/
295 |
296 | # Windows Installer files
297 | *.cab
298 | *.msi
299 | *.msm
300 | *.msp
301 |
302 | # Windows shortcuts
303 | *.lnk
304 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 OUTMATIC Alessandro Petrelli
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MongoDbCache.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.26730.3
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{BD7460C6-FF01-4BD9-9E67-3C8F89F330DA}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8ECC1B6E-FE95-4F4A-A08D-C1AC7642D2E9}"
9 | ProjectSection(SolutionItems) = preProject
10 | README.md = README.md
11 | EndProjectSection
12 | EndProject
13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongoDbCache", "src\MongoDbCache\MongoDbCache.csproj", "{71D72C3E-586C-481E-9D49-B2A37C1E4C01}"
14 | EndProject
15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MongoDbCache.Tests", "src\MongoDbCache.Tests\MongoDbCache.Tests.csproj", "{7F36EE89-88C6-4EA1-B96C-20ABC78C9ACE}"
16 | EndProject
17 | Global
18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
19 | Debug|Any CPU = Debug|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {71D72C3E-586C-481E-9D49-B2A37C1E4C01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {71D72C3E-586C-481E-9D49-B2A37C1E4C01}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {71D72C3E-586C-481E-9D49-B2A37C1E4C01}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {71D72C3E-586C-481E-9D49-B2A37C1E4C01}.Release|Any CPU.Build.0 = Release|Any CPU
27 | {7F36EE89-88C6-4EA1-B96C-20ABC78C9ACE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
28 | {7F36EE89-88C6-4EA1-B96C-20ABC78C9ACE}.Debug|Any CPU.Build.0 = Debug|Any CPU
29 | {7F36EE89-88C6-4EA1-B96C-20ABC78C9ACE}.Release|Any CPU.ActiveCfg = Release|Any CPU
30 | {7F36EE89-88C6-4EA1-B96C-20ABC78C9ACE}.Release|Any CPU.Build.0 = Release|Any CPU
31 | EndGlobalSection
32 | GlobalSection(SolutionProperties) = preSolution
33 | HideSolutionNode = FALSE
34 | EndGlobalSection
35 | GlobalSection(NestedProjects) = preSolution
36 | {71D72C3E-586C-481E-9D49-B2A37C1E4C01} = {BD7460C6-FF01-4BD9-9E67-3C8F89F330DA}
37 | {7F36EE89-88C6-4EA1-B96C-20ABC78C9ACE} = {BD7460C6-FF01-4BD9-9E67-3C8F89F330DA}
38 | EndGlobalSection
39 | GlobalSection(ExtensibilityGlobals) = postSolution
40 | SolutionGuid = {EA67A687-7751-4EE9-A17E-C911EE052B95}
41 | EndGlobalSection
42 | EndGlobal
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MongoDbCache
2 | A distributed cache implementation based on MongoDb, inspired by RedisCache and SqlServerCache (see https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed).
3 |
4 | ### How do I get started?
5 |
6 | Install the nuget package
7 |
8 | PM> Install-Package MongoDbCache
9 |
10 | You can either choose to use the provided extension method or register the implementation in the ConfigureServices method.
11 | The mongo connection settings can be passed as either a connection string or MongoClientSettings object.
12 |
13 | ```csharp
14 | public void ConfigureServices(IServiceCollection services)
15 | {
16 | services.AddMongoDbCache(options =>
17 | {
18 | options.ConnectionString = "mongodb://localhost:27017";
19 | options.DatabaseName = "MongoCache";
20 | options.CollectionName = "appcache";
21 | options.ExpiredScanInterval = TimeSpan.FromMinutes(10);
22 | });
23 | }
24 | ```
25 | ```csharp
26 | public void ConfigureServices(IServiceCollection services)
27 | {
28 | var mongoSettings = new MongoClientSettings();
29 |
30 | services.AddMongoDbCache(options =>
31 | {
32 | options.MongoClientSettings = mongoSettings;
33 | options.DatabaseName = "MongoCache";
34 | options.CollectionName = "appcache";
35 | options.ExpiredScanInterval = TimeSpan.FromMinutes(10;
36 | });
37 | }
38 | ```
39 |
40 | MongoDbCache implements IDistributedCache, therefore you can use all the sync and async methods provided by the interface, please see https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed.
41 |
--------------------------------------------------------------------------------
/src/MongoDbCache.Tests/Infrastructure/MongoDbCacheConfig.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Caching.Distributed;
2 | using System;
3 | using MongoDB.Driver;
4 |
5 | namespace MongoDbCache.Tests.Infrastructure
6 | {
7 | public static class MongoDbCacheConfig
8 | {
9 | public static IDistributedCache CreateCacheInstance()
10 | {
11 | var useMongoClientSettings =
12 | Environment.GetEnvironmentVariable("MongoDbCacheTestsUseMongoClientSettings") == "true";
13 |
14 | return new MongoDbCache(useMongoClientSettings ? CreateOptionsWithMongoClientSettings() : CreateOptions());
15 | }
16 |
17 | public static MongoDbCacheOptions CreateOptions()
18 | {
19 | return new MongoDbCacheOptions
20 | {
21 | ConnectionString = "mongodb://localhost:27017",
22 | DatabaseName = "MongoCache",
23 | CollectionName = "appcache",
24 | ExpiredScanInterval = TimeSpan.FromMinutes(10)
25 | };
26 | }
27 |
28 | private static MongoDbCacheOptions CreateOptionsWithMongoClientSettings()
29 | {
30 | return new MongoDbCacheOptions
31 | {
32 | MongoClientSettings = new MongoClientSettings
33 | {
34 | Server = MongoServerAddress.Parse("localhost")
35 | },
36 | DatabaseName = "MongoCache",
37 | CollectionName = "appcache",
38 | ExpiredScanInterval = TimeSpan.FromMinutes(10)
39 | };
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/MongoDbCache.Tests/MongoDbCache.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 | false
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | all
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/MongoDbCache.Tests/MongoDbCacheServiceExtensionsTests.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.Extensions.Caching.Distributed;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using MongoDbCache.Tests.Infrastructure;
5 | using Xunit;
6 |
7 | namespace MongoDbCache.Tests
8 | {
9 | public class MongoDbCacheServiceExtensionsTests
10 | {
11 | [Fact]
12 | public void AddMongoDbCache_RegistersDistributedCacheAsSingleton()
13 | {
14 | // Arrange
15 | var services = new ServiceCollection();
16 |
17 | // Act
18 | services.AddMongoDbCache(options => {
19 | options = MongoDbCacheConfig.CreateOptions();
20 | });
21 |
22 | // Assert
23 | var distributedCache = services.FirstOrDefault(desc => desc.ServiceType == typeof(IDistributedCache));
24 |
25 | Assert.NotNull(distributedCache);
26 | Assert.Equal(ServiceLifetime.Singleton, distributedCache.Lifetime);
27 | }
28 |
29 | [Fact]
30 | public void AddMongoDbCache_ReplaceUserRegisteredServices()
31 | {
32 | // Arrange
33 | var services = new ServiceCollection();
34 | services.AddSingleton();
35 |
36 | var defaultOptions = MongoDbCacheConfig.CreateOptions();
37 |
38 | // Act
39 | services.AddMongoDbCache(options => {
40 | options.ConnectionString = defaultOptions.ConnectionString;
41 | options.DatabaseName = defaultOptions.DatabaseName;
42 | options.CollectionName = defaultOptions.CollectionName;
43 | options.ExpiredScanInterval = defaultOptions.ExpiredScanInterval;
44 | });
45 |
46 | // Assert
47 | var serviceProvider = services.BuildServiceProvider();
48 |
49 | var distributedCache = services.FirstOrDefault(desc => desc.ServiceType == typeof(IDistributedCache));
50 |
51 | Assert.NotNull(distributedCache);
52 | Assert.Equal(ServiceLifetime.Singleton, distributedCache.Lifetime);
53 | Assert.IsType(serviceProvider.GetRequiredService());
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/MongoDbCache.Tests/MongoDbCacheSetAndRemoveTests.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Caching.Distributed;
2 | using MongoDbCache.Tests.Infrastructure;
3 | using Xunit;
4 |
5 | namespace MongoDbCache.Tests
6 | {
7 | public class MongoDbCacheSetAndRemoveTests
8 | {
9 | [Fact]
10 | public void GetMissingKeyReturnsNull()
11 | {
12 | var cache = MongoDbCacheConfig.CreateCacheInstance();
13 | const string key = "non-existent-key";
14 |
15 | var result = cache.Get(key);
16 | Assert.Null(result);
17 | }
18 |
19 | [Fact]
20 | public void SetAndGetReturnsObject()
21 | {
22 | var cache = MongoDbCacheConfig.CreateCacheInstance();
23 |
24 | var value = new byte[1];
25 | const string key = "myKey";
26 |
27 | cache.Set(key, value);
28 |
29 | var result = cache.Get(key);
30 | Assert.Equal(value, result);
31 | }
32 |
33 | [Fact]
34 | public void SetAndGetWorksWithCaseSensitiveKeys()
35 | {
36 | var cache = MongoDbCacheConfig.CreateCacheInstance();
37 | var value = new byte[1];
38 | const string key1 = "myKey";
39 | const string key2 = "Mykey";
40 |
41 | cache.Set(key1, value);
42 |
43 | var result = cache.Get(key1);
44 | Assert.Equal(value, result);
45 |
46 | result = cache.Get(key2);
47 | Assert.Null(result);
48 | }
49 |
50 | [Fact]
51 | public void SetAlwaysOverwrites()
52 | {
53 | var cache = MongoDbCacheConfig.CreateCacheInstance();
54 | var value1 = new byte[] { 1 };
55 | const string key = "myKey";
56 |
57 | cache.Set(key, value1);
58 | var result = cache.Get(key);
59 | Assert.Equal(value1, result);
60 |
61 | var value2 = new byte[] { 2 };
62 | cache.Set(key, value2);
63 | result = cache.Get(key);
64 | Assert.Equal(value2, result);
65 | }
66 |
67 | [Fact]
68 | public void RemoveRemoves()
69 | {
70 | var cache = MongoDbCacheConfig.CreateCacheInstance();
71 | var value = new byte[1];
72 | const string key = "myKey";
73 |
74 | cache.Set(key, value);
75 | var result = cache.Get(key);
76 | Assert.Equal(value, result);
77 |
78 | cache.Remove(key);
79 | result = cache.Get(key);
80 | Assert.Null(result);
81 | }
82 |
83 | [Fact]
84 | public void RefreshRefreshes()
85 | {
86 | var cache = MongoDbCacheConfig.CreateCacheInstance();
87 | var value = new byte[1];
88 | const string key = "myKeyToBeRefreshed";
89 |
90 | cache.Set(key, value);
91 | cache.Refresh(key);
92 | var result = cache.Get(key);
93 | Assert.Equal(value, result);
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/MongoDbCache.Tests/TestDistributedCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.Extensions.Caching.Distributed;
6 |
7 | namespace MongoDbCache.Tests
8 | {
9 | internal class TestDistributedCache : IDistributedCache
10 | {
11 | public void Connect()
12 | => throw new NotImplementedException();
13 |
14 | public Task ConnectAsync()
15 | => throw new NotImplementedException();
16 |
17 | public byte[] Get(string key)
18 | => throw new NotImplementedException();
19 |
20 | public Task GetAsync(string key, CancellationToken token = default)
21 | => throw new NotImplementedException();
22 |
23 | public void Refresh(string key)
24 | => throw new NotImplementedException();
25 |
26 | public Task RefreshAsync(string key, CancellationToken token = default)
27 | => throw new NotImplementedException();
28 |
29 | public void Remove(string key)
30 | => throw new NotImplementedException();
31 |
32 | public Task RemoveAsync(string key, CancellationToken token = default)
33 | => throw new NotImplementedException();
34 |
35 | public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
36 | => throw new NotImplementedException();
37 |
38 | public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
39 | => throw new NotImplementedException();
40 |
41 | public bool TryGetValue(string key, out Stream value)
42 | => throw new NotImplementedException();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/MongoDbCache.Tests/TimeExpirationTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using Microsoft.Extensions.Caching.Distributed;
4 | using MongoDbCache.Tests.Infrastructure;
5 | using Xunit;
6 |
7 | namespace MongoDbCache.Tests
8 | {
9 | public class TimeExpirationTests
10 | {
11 |
12 | [Fact]
13 | public void AbsoluteExpirationExpires()
14 | {
15 | var cache = MongoDbCacheConfig.CreateCacheInstance();
16 | const string key = "myKey1";
17 | var value = new byte[1];
18 |
19 | cache.Set(key, value, new DistributedCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(1)));
20 |
21 | var result = cache.Get(key);
22 |
23 | Assert.Equal(value, result);
24 |
25 | for (var i = 0; i < 4 && (result != null); i++)
26 | {
27 | Thread.Sleep(TimeSpan.FromSeconds(0.5));
28 | result = cache.Get(key);
29 | }
30 |
31 | Assert.Null(result);
32 | }
33 |
34 |
35 | [Fact]
36 | public void SlidingExpirationExpiresIfNotAccessed()
37 | {
38 | var cache = MongoDbCacheConfig.CreateCacheInstance();
39 | const string key = "myKey2";
40 | var value = new byte[1];
41 |
42 | cache.Set(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(1)));
43 |
44 | var result = cache.Get(key);
45 | Assert.Equal(value, result);
46 |
47 | Thread.Sleep(TimeSpan.FromSeconds(3));
48 |
49 | result = cache.Get(key);
50 | Assert.Null(result);
51 | }
52 |
53 | [Fact]
54 | public void SlidingExpirationRenewedByAccess()
55 | {
56 | var cache = MongoDbCacheConfig.CreateCacheInstance();
57 | const string key = "myKey3";
58 | var value = new byte[1];
59 |
60 | cache.Set(key, value, new DistributedCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromSeconds(1)));
61 |
62 | var result = cache.Get(key);
63 | Assert.Equal(value, result);
64 |
65 | for (var i = 0; i < 5; i++)
66 | {
67 | Thread.Sleep(TimeSpan.FromSeconds(0.5));
68 |
69 | result = cache.Get(key);
70 | Assert.Equal(value, result);
71 | }
72 |
73 | Thread.Sleep(TimeSpan.FromSeconds(3));
74 | result = cache.Get(key);
75 | Assert.Null(result);
76 | }
77 |
78 | [Fact]
79 | public void SlidingExpirationRenewedByAccessUntilAbsoluteExpiration()
80 | {
81 | var cache = MongoDbCacheConfig.CreateCacheInstance();
82 | const string key = "myKey4";
83 | var value = new byte[1];
84 |
85 | cache.Set(key, value, new DistributedCacheEntryOptions()
86 | .SetSlidingExpiration(TimeSpan.FromSeconds(1))
87 | .SetAbsoluteExpiration(TimeSpan.FromSeconds(3)));
88 |
89 | var result = cache.Get(key);
90 | Assert.Equal(value, result);
91 |
92 | for (var i = 0; i < 5; i++)
93 | {
94 | Thread.Sleep(TimeSpan.FromSeconds(0.5));
95 |
96 | result = cache.Get(key);
97 | Assert.Equal(value, result);
98 | }
99 |
100 | Thread.Sleep(TimeSpan.FromSeconds(1));
101 |
102 | result = cache.Get(key);
103 | Assert.Null(result);
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/MongoDbCache/CacheItem.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using MongoDB.Bson.Serialization.Attributes;
3 |
4 | namespace MongoDbCache
5 | {
6 | internal class CacheItem
7 | {
8 | [BsonId]
9 | public string Key { get; }
10 |
11 | [BsonElement("v")]
12 | public byte[] Value { get; }
13 |
14 | [BsonElement("e")]
15 | public DateTimeOffset? ExpiresAt { get; private set; }
16 |
17 | [BsonElement("a")]
18 | public DateTimeOffset? AbsoluteExpiration { get; }
19 |
20 | [BsonElement("s")]
21 | public double? SlidingExpirationInSeconds { get; }
22 |
23 | [BsonConstructor]
24 | public CacheItem(string key, byte[] value, DateTimeOffset? expiresAt, DateTimeOffset? absoluteExpiration, double? slidingExpirationInSeconds)
25 | {
26 | Key = key;
27 | Value = value;
28 | ExpiresAt = expiresAt;
29 | AbsoluteExpiration = absoluteExpiration;
30 | SlidingExpirationInSeconds = slidingExpirationInSeconds;
31 | }
32 |
33 | [BsonConstructor]
34 | public CacheItem(string key, DateTimeOffset? expiresAt, DateTimeOffset? absoluteExpiration, double? slidingExpirationInSeconds)
35 | : this(key, null, expiresAt, absoluteExpiration, slidingExpirationInSeconds)
36 | {
37 |
38 | }
39 |
40 | public CacheItem WithExpiresAt(DateTimeOffset? expiresAt)
41 | {
42 | ExpiresAt = expiresAt;
43 | return this;
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/src/MongoDbCache/MongoContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using MongoDB.Driver;
4 | using System.Threading;
5 | using Microsoft.Extensions.Caching.Distributed;
6 |
7 | namespace MongoDbCache
8 | {
9 | internal class MongoContext
10 | {
11 | private readonly IMongoCollection _collection;
12 |
13 | private static FilterDefinition FilterByKey(string key)
14 | => Builders.Filter.Eq(x => x.Key, key);
15 |
16 | private static FilterDefinition FilterByExpiresAtNotNull()
17 | => Builders.Filter.Ne(x => x.ExpiresAt, null);
18 |
19 | private IFindFluent GetItemQuery(string key, bool withoutValue)
20 | {
21 | var query = _collection.Find(FilterByKey(key));
22 | if (withoutValue)
23 | query = query.Project(Builders.Projection.Exclude(x => x.Value));
24 |
25 | return query;
26 | }
27 |
28 | private static bool CheckIfExpired(DateTimeOffset utcNow, CacheItem cacheItem)
29 | => cacheItem?.ExpiresAt <= utcNow;
30 |
31 | private static DateTimeOffset? GetExpiresAt(DateTimeOffset utcNow, double? slidingExpirationInSeconds, DateTimeOffset? absoluteExpiration)
32 | {
33 | if (slidingExpirationInSeconds == null && absoluteExpiration == null)
34 | return null;
35 |
36 | if (slidingExpirationInSeconds == null)
37 | return absoluteExpiration;
38 |
39 | var seconds = slidingExpirationInSeconds.GetValueOrDefault();
40 |
41 | return utcNow.AddSeconds(seconds) > absoluteExpiration
42 | ? absoluteExpiration
43 | : utcNow.AddSeconds(seconds);
44 | }
45 |
46 | private CacheItem UpdateExpiresAtIfRequired(DateTimeOffset utcNow, CacheItem cacheItem)
47 | {
48 | if (cacheItem.ExpiresAt == null)
49 | return cacheItem;
50 |
51 | var absoluteExpiration = GetExpiresAt(utcNow, cacheItem.SlidingExpirationInSeconds, cacheItem.AbsoluteExpiration);
52 | _collection.UpdateOne(FilterByKey(cacheItem.Key) & FilterByExpiresAtNotNull(),
53 | Builders.Update.Set(x => x.ExpiresAt, absoluteExpiration));
54 |
55 | return cacheItem.WithExpiresAt(absoluteExpiration);
56 | }
57 |
58 | private async Task UpdateExpiresAtIfRequiredAsync(DateTimeOffset utcNow, CacheItem cacheItem)
59 | {
60 | if (cacheItem.ExpiresAt == null)
61 | return cacheItem;
62 |
63 | var absoluteExpiration = GetExpiresAt(utcNow, cacheItem.SlidingExpirationInSeconds, cacheItem.AbsoluteExpiration);
64 | await _collection.UpdateOneAsync(FilterByKey(cacheItem.Key) & FilterByExpiresAtNotNull(),
65 | Builders.Update.Set(x => x.ExpiresAt, absoluteExpiration));
66 |
67 | return cacheItem.WithExpiresAt(absoluteExpiration);
68 | }
69 |
70 | public MongoContext(string connectionString, MongoClientSettings mongoClientSettings, string databaseName, string collectionName)
71 | {
72 | var client = mongoClientSettings == null ? new MongoClient(connectionString) : new MongoClient(mongoClientSettings);
73 | var database = client.GetDatabase(databaseName);
74 |
75 | var expireAtIndexModel = new IndexKeysDefinitionBuilder().Ascending(p => p.ExpiresAt);
76 |
77 | _collection = database.GetCollection(collectionName);
78 |
79 | _collection.Indexes.CreateOne(new CreateIndexModel(expireAtIndexModel, new CreateIndexOptions
80 | {
81 | Background = true
82 | }));
83 | }
84 |
85 | public void DeleteExpired(DateTimeOffset utcNow)
86 | => _collection.DeleteMany(Builders.Filter.Lte(x => x.ExpiresAt, utcNow));
87 |
88 | public byte[] GetCacheItem(string key, bool withoutValue)
89 | {
90 | var utcNow = DateTimeOffset.UtcNow;
91 |
92 | if (key == null)
93 | return null;
94 |
95 | var query = GetItemQuery(key, withoutValue);
96 | var cacheItem = query.SingleOrDefault();
97 | if (cacheItem == null)
98 | return null;
99 |
100 | if (CheckIfExpired(utcNow, cacheItem))
101 | {
102 | Remove(cacheItem.Key);
103 | return null;
104 | }
105 |
106 | cacheItem = UpdateExpiresAtIfRequired(utcNow, cacheItem);
107 |
108 | return cacheItem?.Value;
109 | }
110 |
111 | public async Task GetCacheItemAsync(string key, bool withoutValue, CancellationToken token = default)
112 | {
113 | var utcNow = DateTimeOffset.UtcNow;
114 |
115 | if (key == null)
116 | return null;
117 |
118 | var query = GetItemQuery(key, withoutValue);
119 | var cacheItem = await query.SingleOrDefaultAsync(token);
120 | if (cacheItem == null)
121 | return null;
122 |
123 | if (CheckIfExpired(utcNow, cacheItem))
124 | {
125 | await RemoveAsync(cacheItem.Key, token);
126 | return null;
127 | }
128 |
129 | cacheItem = await UpdateExpiresAtIfRequiredAsync(utcNow, cacheItem);
130 |
131 | return cacheItem?.Value;
132 | }
133 |
134 | public void Set(string key, byte[] value, DistributedCacheEntryOptions options = null)
135 | {
136 | var utcNow = DateTimeOffset.UtcNow;
137 |
138 | if (key == null)
139 | throw new ArgumentNullException(nameof(key));
140 |
141 | if (value == null)
142 | throw new ArgumentNullException(nameof(value));
143 |
144 | var absolutExpiration = options?.AbsoluteExpiration;
145 | var slidingExpirationInSeconds = options?.SlidingExpiration?.TotalSeconds;
146 |
147 | if (options?.AbsoluteExpirationRelativeToNow != null)
148 | absolutExpiration = utcNow.Add(options.AbsoluteExpirationRelativeToNow.Value);
149 |
150 | if (absolutExpiration <= utcNow)
151 | throw new InvalidOperationException("The absolute expiration value must be in the future.");
152 |
153 | var expiresAt = GetExpiresAt(utcNow, slidingExpirationInSeconds, absolutExpiration);
154 | var cacheItem = new CacheItem(key, value, expiresAt, absolutExpiration, slidingExpirationInSeconds);
155 |
156 | _collection.ReplaceOne(FilterByKey(key), cacheItem, new ReplaceOptions
157 | {
158 | IsUpsert = true
159 | });
160 | }
161 |
162 | public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options = null, CancellationToken token = default)
163 | {
164 | var utcNow = DateTimeOffset.UtcNow;
165 |
166 | if (key == null)
167 | throw new ArgumentNullException(nameof(key));
168 |
169 | if (value == null)
170 | throw new ArgumentNullException(nameof(value));
171 |
172 | var absolutExpiration = options?.AbsoluteExpiration;
173 | var slidingExpirationInSeconds = options?.SlidingExpiration?.TotalSeconds;
174 |
175 | if (options?.AbsoluteExpirationRelativeToNow != null)
176 | absolutExpiration = utcNow.Add(options.AbsoluteExpirationRelativeToNow.Value);
177 |
178 | if (absolutExpiration <= utcNow)
179 | throw new InvalidOperationException("The absolute expiration value must be in the future.");
180 |
181 | var expiresAt = GetExpiresAt(utcNow, slidingExpirationInSeconds, absolutExpiration);
182 | var cacheItem = new CacheItem(key, value, expiresAt, absolutExpiration, slidingExpirationInSeconds);
183 |
184 | await _collection.ReplaceOneAsync(FilterByKey(key), cacheItem, new ReplaceOptions
185 | {
186 | IsUpsert = true
187 | }, token);
188 | }
189 |
190 | public void Remove(string key)
191 | => _collection.DeleteOne(FilterByKey(key));
192 |
193 | public async Task RemoveAsync(string key, CancellationToken token = default)
194 | => await _collection.DeleteOneAsync(FilterByKey(key), token);
195 | }
196 | }
--------------------------------------------------------------------------------
/src/MongoDbCache/MongoDbCache.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.Caching.Distributed;
5 | using Microsoft.Extensions.Options;
6 |
7 | namespace MongoDbCache
8 | {
9 | public class MongoDbCache : IDistributedCache
10 | {
11 | private DateTimeOffset _lastScan = DateTimeOffset.UtcNow;
12 | private TimeSpan _scanInterval;
13 | private readonly TimeSpan _defaultScanInterval = TimeSpan.FromMinutes(5);
14 | private readonly MongoContext _mongoContext;
15 |
16 | private static void ValidateOptions(MongoDbCacheOptions cacheOptions)
17 | {
18 | if (!string.IsNullOrEmpty(cacheOptions.ConnectionString) && cacheOptions.MongoClientSettings != null)
19 | throw new ArgumentException($"Only one of {nameof(cacheOptions.ConnectionString)} and {nameof(cacheOptions.MongoClientSettings)} can be set.");
20 |
21 | if (string.IsNullOrEmpty(cacheOptions.ConnectionString) && cacheOptions.MongoClientSettings == null)
22 | throw new ArgumentException($"{nameof(cacheOptions.ConnectionString)} or {nameof(cacheOptions.MongoClientSettings)} cannot be empty or null.");
23 |
24 | if (string.IsNullOrEmpty(cacheOptions.DatabaseName))
25 | throw new ArgumentException($"{nameof(cacheOptions.DatabaseName)} cannot be empty or null.");
26 |
27 | if (string.IsNullOrEmpty(cacheOptions.CollectionName))
28 | throw new ArgumentException($"{nameof(cacheOptions.CollectionName)} cannot be empty or null.");
29 | }
30 |
31 | private void SetScanInterval(TimeSpan? scanInterval)
32 | {
33 | _scanInterval = scanInterval?.TotalSeconds > 0
34 | ? scanInterval.Value
35 | : _defaultScanInterval;
36 | }
37 |
38 | public MongoDbCache(IOptions optionsAccessor)
39 | {
40 | var options = optionsAccessor.Value;
41 | ValidateOptions(options);
42 |
43 | _mongoContext = new MongoContext(options.ConnectionString, options.MongoClientSettings, options.DatabaseName, options.CollectionName);
44 |
45 | SetScanInterval(options.ExpiredScanInterval);
46 | }
47 |
48 | public byte[] Get(string key)
49 | {
50 | var value = _mongoContext.GetCacheItem(key, withoutValue: false);
51 |
52 | ScanAndDeleteExpired();
53 |
54 | return value;
55 | }
56 |
57 | public void Set(string key, byte[] value, DistributedCacheEntryOptions options = null)
58 | {
59 | _mongoContext.Set(key, value, options);
60 |
61 | ScanAndDeleteExpired();
62 | }
63 |
64 | public void Refresh(string key)
65 | {
66 | _mongoContext.GetCacheItem(key, withoutValue: true);
67 |
68 | ScanAndDeleteExpired();
69 | }
70 |
71 | public async Task GetAsync(string key, CancellationToken token = default)
72 | {
73 | var value = await _mongoContext.GetCacheItemAsync(key, withoutValue: false, token: token);
74 |
75 | ScanAndDeleteExpired();
76 |
77 | return value;
78 | }
79 |
80 | public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
81 | {
82 | await _mongoContext.SetAsync(key, value, options, token);
83 |
84 | ScanAndDeleteExpired();
85 | }
86 |
87 | public async Task RefreshAsync(string key, CancellationToken token = default)
88 | {
89 | await _mongoContext.GetCacheItemAsync(key, withoutValue: true, token: token);
90 |
91 | ScanAndDeleteExpired();
92 | }
93 |
94 | public async Task RemoveAsync(string key, CancellationToken token = default)
95 | {
96 | await _mongoContext.RemoveAsync(key, token);
97 |
98 | ScanAndDeleteExpired();
99 | }
100 |
101 | public void Remove(string key)
102 | {
103 | _mongoContext.Remove(key);
104 |
105 | ScanAndDeleteExpired();
106 | }
107 |
108 | private void ScanAndDeleteExpired()
109 | {
110 | var utcNow = DateTimeOffset.UtcNow;
111 |
112 | if (_lastScan.Add(_scanInterval) < utcNow)
113 | Task.Run(() =>
114 | {
115 | _lastScan = utcNow;
116 | _mongoContext.DeleteExpired(utcNow);
117 | });
118 | }
119 | }
120 | }
--------------------------------------------------------------------------------
/src/MongoDbCache/MongoDbCache.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | 2.5.0
6 | MIT
7 | Copyright (c) OUTMATIC Alessandro Petrelli & Contributors
8 | Alessandro Petrelli & Contributors
9 | OUTMATIC Alessandro Petrelli
10 | https://github.com/outmatic/MongoDbCache
11 | https://github.com/outmatic/MongoDbCache
12 | A distributed cache implementation based on MongoDb, inspired by RedisCache and SqlServerCache
13 | true
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/src/MongoDbCache/MongoDbCacheOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Options;
3 | using MongoDB.Driver;
4 |
5 | namespace MongoDbCache
6 | {
7 | public class MongoDbCacheOptions : IOptions
8 | {
9 | public string ConnectionString { get; set; }
10 | public MongoClientSettings MongoClientSettings { get; set; }
11 | public string DatabaseName { get; set; }
12 | public string CollectionName { get; set; }
13 | public TimeSpan? ExpiredScanInterval { get; set; }
14 |
15 | MongoDbCacheOptions IOptions.Value => this;
16 | }
17 | }
--------------------------------------------------------------------------------
/src/MongoDbCache/MongoDbCacheServicesExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.Extensions.Caching.Distributed;
3 | using Microsoft.Extensions.DependencyInjection;
4 |
5 | namespace MongoDbCache
6 | {
7 | public static class MongoDbCacheServicesExtensions
8 | {
9 | ///
10 | /// Adds MongoDb distributed caching services to the specified .
11 | ///
12 | /// The to add services to.
13 | /// An to configure the provided
14 | /// .
15 | /// The so that additional calls can be chained.
16 | public static IServiceCollection AddMongoDbCache(this IServiceCollection services, Action setupAction)
17 | {
18 | if (services == null)
19 | throw new ArgumentNullException(nameof(services));
20 |
21 | if (setupAction == null)
22 | throw new ArgumentNullException(nameof(setupAction));
23 |
24 | services.AddOptions();
25 | services.Configure(setupAction);
26 | services.Add(ServiceDescriptor.Singleton());
27 |
28 | return services;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------