├── .gitattributes ├── .gitignore ├── .nuget ├── NuGet.Config └── packages.config ├── LICENSE ├── README.md ├── RedisMemoryCacheInvalidation.nuspec ├── RedisMemoryCacheInvalidation.sln ├── appveyor.yml ├── docs ├── Configuration.md ├── Examples.md ├── Overview.md └── Redis.md ├── samples ├── SampleInvalidationEmitter │ ├── App.config │ ├── Program.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── SampleInvalidationEmitter.csproj │ └── packages.config └── SampleWebApplication │ ├── App_Start │ ├── BundleConfig.cs │ ├── FilterConfig.cs │ ├── IdentityConfig.cs │ ├── RouteConfig.cs │ └── Startup.Auth.cs │ ├── Content │ ├── Site.css │ ├── bootstrap-theme.css │ ├── bootstrap-theme.css.map │ ├── bootstrap-theme.min.css │ ├── bootstrap.css │ ├── bootstrap.css.map │ └── bootstrap.min.css │ ├── Controllers │ ├── AccountController.cs │ ├── HomeController.cs │ └── ManageController.cs │ ├── Global.asax │ ├── Global.asax.cs │ ├── Models │ ├── AccountViewModels.cs │ ├── IdentityModels.cs │ └── ManageViewModels.cs │ ├── Project_Readme.html │ ├── Properties │ └── AssemblyInfo.cs │ ├── SampleWebApplication.csproj │ ├── Scripts │ ├── _references.js │ ├── bootstrap.js │ ├── bootstrap.min.js │ ├── jquery-1.10.2.intellisense.js │ ├── jquery-2.1.4-vsdoc.js │ ├── jquery-2.1.4.js │ ├── jquery-2.1.4.min.js │ ├── jquery-2.1.4.min.map │ ├── jquery.validate-vsdoc.js │ ├── jquery.validate.js │ ├── jquery.validate.min.js │ ├── jquery.validate.unobtrusive.js │ ├── jquery.validate.unobtrusive.min.js │ ├── modernizr-2.8.3.js │ ├── respond.js │ ├── respond.matchmedia.addListener.js │ ├── respond.matchmedia.addListener.min.js │ └── respond.min.js │ ├── Startup.cs │ ├── Views │ ├── Account │ │ ├── ConfirmEmail.cshtml │ │ ├── ExternalLoginConfirmation.cshtml │ │ ├── ExternalLoginFailure.cshtml │ │ ├── ForgotPassword.cshtml │ │ ├── ForgotPasswordConfirmation.cshtml │ │ ├── Login.cshtml │ │ ├── Register.cshtml │ │ ├── ResetPassword.cshtml │ │ ├── ResetPasswordConfirmation.cshtml │ │ ├── SendCode.cshtml │ │ ├── VerifyCode.cshtml │ │ └── _ExternalLoginsListPartial.cshtml │ ├── Home │ │ ├── About.cshtml │ │ ├── Contact.cshtml │ │ └── Index.cshtml │ ├── Manage │ │ ├── AddPhoneNumber.cshtml │ │ ├── ChangePassword.cshtml │ │ ├── Index.cshtml │ │ ├── ManageLogins.cshtml │ │ ├── SetPassword.cshtml │ │ └── VerifyPhoneNumber.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── Lockout.cshtml │ │ ├── _Layout.cshtml │ │ └── _LoginPartial.cshtml │ ├── Web.config │ └── _ViewStart.cshtml │ ├── Web.Debug.config │ ├── Web.Release.config │ ├── Web.config │ ├── favicon.ico │ ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 │ └── packages.config ├── scripts ├── pack.ps1 ├── run_tests.ps1 └── run_tests_local.ps1 ├── src ├── RedisMemoryCacheInvalidation │ ├── Constants.cs │ ├── Core │ │ ├── Interfaces │ │ │ ├── INotificationManager.cs │ │ │ ├── INotificationObserver.cs │ │ │ └── IRedisNotificationBus.cs │ │ ├── NotificationManager.cs │ │ ├── RedisNotificationBus.cs │ │ └── Unsubscriber.cs │ ├── InvalidationManager.cs │ ├── InvalidationSettings.cs │ ├── InvalidationStrategyType.cs │ ├── Monitor │ │ └── RedisChangeMonitor.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Redis │ │ ├── ExistingRedisConnnection.cs │ │ ├── IRedisConnection.cs │ │ ├── RedisConnectionBase.cs │ │ ├── RedisConnectionFactory.cs │ │ └── StandaloneRedisConnection.cs │ ├── RedisMemoryCacheInvalidation.csproj │ ├── Utils │ │ ├── Guard.cs │ │ ├── SynchronizedCollection.cs │ │ └── TaskCache.cs │ └── packages.config └── RedisMemoryCacheInvalidation_Net40 │ ├── RedisMemoryCacheInvalidation_Net40.csproj │ └── packages.config ├── tests └── RedisMemoryCacheInvalidation.Tests │ ├── App.config │ ├── Core │ ├── InvalidationManagerTests.cs │ ├── NotificationManagerTests.cs │ ├── RedisNotificationBusTests.cs │ └── UnsubscriberTests.cs │ ├── Fixture │ ├── RedisCollection.cs │ └── RedisServerFixture.cs │ ├── Integration │ ├── DisconnectedTests.cs │ └── InvalidationTests.cs │ ├── Monitor │ └── RedisChangeMonitorTest.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Redis │ ├── ExistingRedisConnectionTests.cs │ ├── RedisConnectionFactoryTests.cs │ └── StandaloneRedisConnectionTests.cs │ ├── RedisMemoryCacheInvalidation.Tests.csproj │ ├── TestConstants.cs │ └── packages.config └── tools ├── Redis Release Notes.docx ├── Redis on Windows.docx ├── RedisService.docx ├── redis-benchmark.exe ├── redis-check-aof.exe ├── redis-check-dump.exe ├── redis-cli.exe ├── redis-server.exe └── redis.windows.conf /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Visual Studio 3 | ################# 4 | 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.sln.docstates 12 | 13 | # Build results 14 | 15 | [Dd]ebug/ 16 | [Rr]elease/ 17 | x64/ 18 | build/ 19 | [Bb]in/ 20 | [Oo]bj/ 21 | .vs/ 22 | 23 | # MSTest test Results 24 | [Tt]est[Rr]esult*/ 25 | [Bb]uild[Ll]og.* 26 | 27 | *_i.c 28 | *_p.c 29 | *.ilk 30 | *.meta 31 | *.obj 32 | *.pch 33 | *.pdb 34 | *.pgc 35 | *.pgd 36 | *.rsp 37 | *.sbr 38 | *.tlb 39 | *.tli 40 | *.tlh 41 | *.tmp 42 | *.tmp_proj 43 | *.log 44 | *.vspscc 45 | *.vssscc 46 | .builds 47 | *.pidb 48 | *.log 49 | *.scc 50 | 51 | # Visual C++ cache files 52 | ipch/ 53 | *.aps 54 | *.ncb 55 | *.opensdf 56 | *.sdf 57 | *.cachefile 58 | 59 | # Visual Studio profiler 60 | *.psess 61 | *.vsp 62 | *.vspx 63 | 64 | # Guidance Automation Toolkit 65 | *.gpState 66 | 67 | # ReSharper is a .NET coding add-in 68 | _ReSharper*/ 69 | *.[Rr]e[Ss]harper 70 | 71 | # TeamCity is a build add-in 72 | _TeamCity* 73 | 74 | # DotCover is a Code Coverage Tool 75 | *.dotCover 76 | 77 | # NCrunch 78 | *.ncrunch* 79 | .*crunch*.local.xml 80 | 81 | # Installshield output folder 82 | [Ee]xpress/ 83 | 84 | # DocProject is a documentation generator add-in 85 | DocProject/buildhelp/ 86 | DocProject/Help/*.HxT 87 | DocProject/Help/*.HxC 88 | DocProject/Help/*.hhc 89 | DocProject/Help/*.hhk 90 | DocProject/Help/*.hhp 91 | DocProject/Help/Html2 92 | DocProject/Help/html 93 | 94 | # Click-Once directory 95 | publish/ 96 | 97 | # Publish Web Output 98 | *.Publish.xml 99 | *.pubxml 100 | 101 | # NuGet Packages Directory 102 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 103 | packages/ 104 | dist/ 105 | 106 | # Windows Azure Build Output 107 | csx 108 | *.build.csdef 109 | 110 | # Windows Store app package directory 111 | AppPackages/ 112 | 113 | # Others 114 | sql/ 115 | *.Cache 116 | ClientBin/ 117 | [Ss]tyle[Cc]op.* 118 | ~$* 119 | *~ 120 | *.dbmdl 121 | *.[Pp]ublish.xml 122 | *.pfx 123 | *.publishsettings 124 | 125 | # RIA/Silverlight projects 126 | Generated_Code/ 127 | 128 | # Backup & report files from converting an old project file to a newer 129 | # Visual Studio version. Backup files are not needed, because we have git ;-) 130 | _UpgradeReport_Files/ 131 | Backup*/ 132 | UpgradeLog*.XML 133 | UpgradeLog*.htm 134 | 135 | # SQL Server files 136 | App_Data/*.mdf 137 | App_Data/*.ldf 138 | 139 | ############# 140 | ## Windows detritus 141 | ############# 142 | 143 | # Windows image file caches 144 | Thumbs.db 145 | ehthumbs.db 146 | 147 | # Folder config file 148 | Desktop.ini 149 | 150 | # Recycle Bin used on file shares 151 | $RECYCLE.BIN/ 152 | 153 | # Mac crap 154 | .DS_Store 155 | 156 | 157 | ############# 158 | ## Python 159 | ############# 160 | 161 | *.py[co] 162 | 163 | # Packages 164 | *.egg 165 | *.egg-info 166 | dist/ 167 | build/ 168 | eggs/ 169 | parts/ 170 | var/ 171 | sdist/ 172 | develop-eggs/ 173 | .installed.cfg 174 | 175 | # Installer logs 176 | pip-log.txt 177 | 178 | # Unit test / coverage reports 179 | .coverage 180 | .tox 181 | 182 | #Translations 183 | *.mo 184 | 185 | #Mr Developer 186 | .mr.developer.cfg 187 | 188 | #redis 189 | .rdb 190 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.nuget/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Cybermaxs 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RedisMemoryCacheInvalidation 2 | ============================ 3 | 4 | [![Build status](https://ci.appveyor.com/api/projects/status/o64bqf543kype8eq?svg=true)](https://ci.appveyor.com/project/Cybermaxs/redismemorycacheinvalidation) 5 | [![Nuget](https://img.shields.io/nuget/dt/redismemorycacheinvalidation.svg)](http://nuget.org/packages/redismemorycacheinvalidation) 6 | [![Nuget](https://img.shields.io/nuget/v/redismemorycacheinvalidation.svg)](http://nuget.org/packages/redismemorycacheinvalidation) 7 | [![Coverage Status](https://coveralls.io/repos/Cybermaxs/RedisMemoryCacheInvalidation/badge.svg?branch=master&service=github)](https://coveralls.io/github/Cybermaxs/RedisMemoryCacheInvalidation?branch=master) 8 | 9 | System.Runtime.MemoryCache invalidation using Redis PubSub feature. 10 | 11 | 12 | Installing via NuGet 13 | --- 14 | ``` 15 | Install-Package RedisMemoryCacheInvalidation 16 | ``` 17 | 18 | 19 | How to use it ? 20 | --- 21 | 22 | __quick start__ 23 | 24 | 25 | First, you have to configure the library, mainly to setup a persistent redis connection and various stuff 26 | ```csharp 27 | // somewhere in your global.asax/startup.cs 28 | InvalidationManager.Configure("localhost:6379", new InvalidationSettings()); 29 | ``` 30 | Redis connection string follow [StackExchange.Redis Configuration model](https://github.com/StackExchange/StackExchange.Redis/blob/master/Docs/Configuration.md) 31 | 32 | Threre are at least 3 ways to send invalidation messages : 33 | - send an invalidation message via any redis client following the command `PUBLISH invalidate onemessagekey` 34 | - use `InvalidationManager.InvalidateAsync` (same as the previous one) 35 | - use keyspace notification (yes, RedisMemoryCacheInvalidation supports it) 36 | 37 | Once an invalidation message is intercepted by the library, you can invalidate one or more items at the same time by using `InvalidationSettings.InvalidationStrategy` 38 | - `InvalidationStrategyType.ChangeMonitor` => a custom custom change monitor `InvalidationManager.CreateChangeMonitor` 39 | - `InvalidationStrategyType.AutoCacheRemoval` => use the automatic MemoryCache removal configured at `InvalidationSettings.ConfigureAsync` 40 | - `InvalidationStrategyType.External` => use the callback configured at `InvalidationSettings.InvalidationCallback` 41 | 42 | __Read more__ 43 | - [Overview] (https://github.com/Cybermaxs/RedisMemoryCacheInvalidation/blob/master/docs/Overview.md) 44 | - [Working with Redis] (https://github.com/Cybermaxs/RedisMemoryCacheInvalidation/blob/master/docs/Redis.md) 45 | - [Configuration] (https://github.com/Cybermaxs/RedisMemoryCacheInvalidation/blob/master/docs/Configuration.md) 46 | - [Examples] (https://github.com/Cybermaxs/RedisMemoryCacheInvalidation/blob/master/docs/Examples.md) 47 | 48 | How it works ? 49 | --- 50 | Read the introduction post for the initial version (beginning of 2014) here : https://techblog.betclicgroup.com/2013/12/31/implementing-local-memorycache-invalidation-with-redis/ 51 | 52 | License 53 | --- 54 | Licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT) 55 | 56 | Want to contribute ? 57 | --- 58 | - Beginner => Download, Star, Comment/Tweet, Kudo, ... 59 | - Amateur => Ask for help, send feature request, send bugs 60 | - Pro => Pull request, promote 61 | 62 | Thank you 63 | -------------------------------------------------------------------------------- /RedisMemoryCacheInvalidation.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | RedisMemoryCacheInvalidation 5 | $version$ 6 | RedisMemoryCacheInvalidation 7 | Cybermaxs 8 | Cybermaxs 9 | false 10 | https://github.com/Cybermaxs/RedisMemoryCacheInvalidation 11 | https://raw.githubusercontent.com/Cybermaxs/RedisMemoryCacheInvalidation/master/LICENSE 12 | This package allow to invalidate local memory cache items with the help of StackExchange.Redis. 13 | A redis-powered local memory cache invalidation mechanism. 14 | Copyright 2015 15 | Redis Cache Invalidation MemoryCache NoSQL PubSub 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /RedisMemoryCacheInvalidation.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.23107.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisMemoryCacheInvalidation", "src\RedisMemoryCacheInvalidation\RedisMemoryCacheInvalidation.csproj", "{13467724-727B-44CA-BE9D-19BFC4599B34}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisMemoryCacheInvalidation.Tests", "tests\RedisMemoryCacheInvalidation.Tests\RedisMemoryCacheInvalidation.Tests.csproj", "{40A11C38-D401-47D2-87A7-35F4019D496B}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{2570F5F6-E99B-4968-A0CC-5008554444B9}" 11 | ProjectSection(SolutionItems) = preProject 12 | .nuget\NuGet.Config = .nuget\NuGet.Config 13 | .nuget\packages.config = .nuget\packages.config 14 | EndProjectSection 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{34FE30CE-858A-4880-8A6E-BD0451757602}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{96F870E2-ADD6-48CB-B04A-F4A1B4357CA6}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FB54E47A-C7AA-4DBB-B575-A45678B4E278}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisMemoryCacheInvalidation_Net40", "src\RedisMemoryCacheInvalidation_Net40\RedisMemoryCacheInvalidation_Net40.csproj", "{CC9DCBE7-B6B8-4F13-9768-B90DB619B8DB}" 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleWebApplication", "samples\SampleWebApplication\SampleWebApplication.csproj", "{C1FB7FF5-7FEF-4770-A570-BA3F578ED0EA}" 25 | EndProject 26 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleInvalidationEmitter", "samples\SampleInvalidationEmitter\SampleInvalidationEmitter.csproj", "{CBAE0FB2-0706-456F-8CF8-6F548F2E4161}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {13467724-727B-44CA-BE9D-19BFC4599B34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {13467724-727B-44CA-BE9D-19BFC4599B34}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {13467724-727B-44CA-BE9D-19BFC4599B34}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {13467724-727B-44CA-BE9D-19BFC4599B34}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {40A11C38-D401-47D2-87A7-35F4019D496B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {40A11C38-D401-47D2-87A7-35F4019D496B}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {40A11C38-D401-47D2-87A7-35F4019D496B}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {40A11C38-D401-47D2-87A7-35F4019D496B}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {CC9DCBE7-B6B8-4F13-9768-B90DB619B8DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {CC9DCBE7-B6B8-4F13-9768-B90DB619B8DB}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {CC9DCBE7-B6B8-4F13-9768-B90DB619B8DB}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {CC9DCBE7-B6B8-4F13-9768-B90DB619B8DB}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {C1FB7FF5-7FEF-4770-A570-BA3F578ED0EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {C1FB7FF5-7FEF-4770-A570-BA3F578ED0EA}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {C1FB7FF5-7FEF-4770-A570-BA3F578ED0EA}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {C1FB7FF5-7FEF-4770-A570-BA3F578ED0EA}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {CBAE0FB2-0706-456F-8CF8-6F548F2E4161}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {CBAE0FB2-0706-456F-8CF8-6F548F2E4161}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {CBAE0FB2-0706-456F-8CF8-6F548F2E4161}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {CBAE0FB2-0706-456F-8CF8-6F548F2E4161}.Release|Any CPU.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(NestedProjects) = preSolution 59 | {13467724-727B-44CA-BE9D-19BFC4599B34} = {FB54E47A-C7AA-4DBB-B575-A45678B4E278} 60 | {40A11C38-D401-47D2-87A7-35F4019D496B} = {34FE30CE-858A-4880-8A6E-BD0451757602} 61 | {CC9DCBE7-B6B8-4F13-9768-B90DB619B8DB} = {FB54E47A-C7AA-4DBB-B575-A45678B4E278} 62 | {C1FB7FF5-7FEF-4770-A570-BA3F578ED0EA} = {96F870E2-ADD6-48CB-B04A-F4A1B4357CA6} 63 | {CBAE0FB2-0706-456F-8CF8-6F548F2E4161} = {96F870E2-ADD6-48CB-B04A-F4A1B4357CA6} 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.1.{build} 2 | 3 | assembly_info: 4 | patch: true 5 | file: AssemblyInfo.* 6 | assembly_version: "{version}" 7 | assembly_file_version: "{version}" 8 | assembly_informational_version: "{version}" 9 | 10 | configuration: Release 11 | 12 | build: 13 | verbosity: minimal 14 | publish_nuget: true 15 | project: RedisMemoryCacheInvalidation.sln 16 | 17 | before_build: 18 | - nuget restore 19 | after_build: 20 | - ps: .\scripts\pack.ps1 21 | 22 | artifacts: 23 | - path: '*.nupkg' # find all NuGet packages recursively 24 | 25 | test: 26 | categories: 27 | except: 28 | - Integration 29 | 30 | test_script: 31 | - ps: .\scripts\run_tests.ps1 32 | -------------------------------------------------------------------------------- /docs/Configuration.md: -------------------------------------------------------------------------------- 1 | Configuration 2 | = 3 | 4 | Settings 5 | --- 6 | To configure `RedisMemoryCacheInvalidation`, you should use one of the `InvalidationManager.ConfigureAsync` methods. 7 | Three parameters are available to configure it : 8 | 9 | - __redisConfig:string__ : Redis connection string. Check [StackExchange.Redis Configuration model](https://github.com/StackExchange/StackExchange.Redis/blob/master/Docs/Configuration.md) for more details. A basic example is `localhost:6379`. 10 | - __mux:ConnectionMultiplexer__ : an existing StackExchange.Redis.ConnectionMultiplexer that you want to reuse. 11 | - __settings:InvalidationSettings__ : see below for more details. 12 | 13 | InvalidationSettings is the main configuration object 14 | - __InvalidationStrategy:InvalidationStrategyType__ : How to handle invalidation notifications : notify ChangeMonitor, execute callback or automatically remove an item from the cache. 15 | - __TargetCache:MemoryCache__ : the target MemoryCache instance when `InvalidationStrategy` is set to `AutoCacheRemoval`. 16 | - __EnableKeySpaceNotifications:bool__ : allow subscribe to keyevents notification `__keyevent*__:`. 17 | - __InvalidationCallback:Action__ : an callback that is invoked when `InvalidationStrategy` is set to `External`. 18 | 19 | When to configure ? 20 | --- 21 | Thanks to StackExchange.Redis a persistent connection is established between your application and the redis server. 22 | That's why it's important to configure it very early at startup : Global.asax, Owin Startup, ... In this way, you won't lose notification messages. -------------------------------------------------------------------------------- /docs/Examples.md: -------------------------------------------------------------------------------- 1 | How to use 2 | = 3 | 4 | Once RedisMemoryCacheInvalidation is configured, local cache invalidation is a two-steps process : capturing invalidation messages and handling those notification messages. 5 | 6 | 7 | Sending invalidation messages 8 | --- 9 | You can use one of the folowing methods. 10 | 11 | - Send a pubsub message from any redis client `PUBLISH invalidate onemessagekey`. 12 | - Send an invalidation message from `InvalidationManager.InvalidateAsync("onemessagekey")` 13 | - Capture keyspace events for one particular key. Note : the redis server should be configured to support keyspace events. (off by default) 14 | 15 | Handling notification messages 16 | --- 17 | This behavior is entirely configured via `InvalidationSettings.InvalidationStrategyType`. As it's marked with a FlagsAttribute, you can use one or more strategies. 18 | 19 | - Automatically removed a cache item from the cache 20 | 21 | The easiest way to invalidate local cache items. If the The core will try to remove cache items 22 | For example, if you add a cache item like this : 23 | 24 | ``` 25 | CacheItem cacheItem = new CacheItem("mycacheKey", "cachevalue"); 26 | CacheItemPolicy policy = new CacheItemPolicy(); 27 | policy.AbsoluteExpiration = DateTime.UtcNow.AddDays(1); 28 | MyCache.Add(cacheItem, policy); 29 | ``` 30 | 31 | Calling `PUBLISH invalidate mycacheKey` or `InvalidationManager.InvalidateAsync("mycacheKey")` will remove that item from the cache. 32 | 33 | - Notify ChangeMonitors 34 | 35 | [ChangeMonitor](http://msdn.microsoft.com/en-us/library/system.runtime.caching.changemonitor(v=vs.110).aspx) is defined as "Provides a base class for a derived custom type that monitors changes in the state of the data which a cache item depends on."" 36 | 37 | You can create a custom monitor (watching for `myinvalidationKey`) like this : 38 | 39 | ``` 40 | CacheItem cacheItem = new CacheItem("cacheKey", "cachevalue"); 41 | CacheItemPolicy policy = new CacheItemPolicy(); 42 | policy.AbsoluteExpiration = DateTime.UtcNow.AddDays(1); 43 | policy.ChangeMonitors.Add(InvalidationManager.CreateChangeMonitor("myinvalidationKey")); 44 | MyCache.Add(cacheItem, policy); 45 | ``` 46 | 47 | When , the corresponding cache item will be automatically removed. 48 | One interesting feature is that you can create several change monitors watching for the same key. 49 | 50 | - invoke a callback 51 | 52 | Suppose you're using another caching implementation (Entlib, System.Web.Caching, ...), there is another way to be notified with `InvalidationStrategyType.External`. 53 | Each time a notification message is intercepted, the callback defined in `InvalidationSettings.InvalidationCallback` is invoked. 54 | It's up to you to remove/flush/reload the cache item. 55 | -------------------------------------------------------------------------------- /docs/Overview.md: -------------------------------------------------------------------------------- 1 | Overview 2 | = 3 | 4 | We need to use all the tools at our disposal to develop faster and more robust applications. 5 | One of the ways we can achieve this is by using caching. Previously, under the `System.Web.Caching.Cache` namespace, the new -4.0- `System.Runtime.Caching.Memory` is more mature and still as powerful as its ancestor. 6 | It’s really easy to use and I think that every asp.net programmers have already used it. 7 | 8 | The default caching strategy is the cache-aside programming pattern: This means that if your data is not present in the cache, your application, and not something else, must reload data into the cache from the original data source. 9 | 10 | This works very well for most common use cases, but there is a hidden trade-off: caching means working with stale data. Should I increase the cache duration? Should I keep short TTL value? It’s never easy to answer to these questions, because it simply depends on your context: number of clients, user load, database activity… 11 | 12 | Another common problem is how to update/remove instantly something from the cache on all your cache clients, such as this typical request from Product Owner ” I want a parameter to be instantly updated on all our XXX servers, but do not fear, I will only change it a few times per year” 13 | 14 | Here comes `RedisMemoryCacheInlivation` : a small library that will help you to invalidate one or more items in you local memory cache. 15 | 16 | Supports .net 4.0 and later. 17 | Supports any kind of .net application (asp.net WebForms & MVC, WPF & WinForms, Console, ...). 18 | 19 | As the name suggests, this implementation relies on [Redis](http://redis.io/), especially on [PubSub feature](http://redis.io/topics/pubsub). 20 | In the current version of this implemention, __nothing is stored__ on the Redis server. 21 | -------------------------------------------------------------------------------- /docs/Redis.md: -------------------------------------------------------------------------------- 1 | Redis 2 | = 3 | 4 | 5 | Working Redis on Windows 6 | --- 7 | The Redis project does not directly support Windows, however the Microsoft Open Tech group develops and maintains an [Windows port targeting Win64](https://github.com/MSOpenTech/redis). 8 | So, the folder `tools` contains an exact copy of the nuget package [Redis-64](http://www.nuget.org/packages/Redis-64/) maintenaned by MsOpenTech. 9 | This is the easiest way to test locally this NoSql DB on Windows. Simply run `redis-server.exe` and that's all. In this case, the connection string is `localhost:6379`. 10 | Please note that this port is also used on Windows Azure, so it's prod-ready. 11 | 12 | 13 | on Production 14 | --- 15 | You just need a redis server and it doesn't matter on the hosting platform. If you don't have Redis into you stack, there are several -cloud- providers like Azure, Redis to Go or Redis Watch. -------------------------------------------------------------------------------- /samples/SampleInvalidationEmitter/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /samples/SampleInvalidationEmitter/Program.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation; 2 | using System; 3 | 4 | namespace SampleInvalidationEmitter 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | Console.WriteLine("Simple Invalidation Emitter"); 11 | 12 | InvalidationManager.Configure("localhost:6379", new InvalidationSettings()); 13 | 14 | Console.WriteLine("IsConnected : "+ InvalidationManager.IsConnected); 15 | 16 | Console.WriteLine("enter a key to send invalidation (default is 'mynotifmessage'): "); 17 | var key = Console.ReadLine(); 18 | var task = InvalidationManager.InvalidateAsync(string.IsNullOrEmpty(key) ? "mynotifmessage": key); 19 | 20 | Console.WriteLine("message send to {0} clients", task.Result); 21 | Console.ReadLine(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/SampleInvalidationEmitter/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("SampleInvalidationEmitter")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("SampleInvalidationEmitter")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("adf5ba85-25f9-4020-9c34-512f8a04df4d")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.14474.12")] 36 | [assembly: AssemblyFileVersion("1.0.14474.12")] 37 | -------------------------------------------------------------------------------- /samples/SampleInvalidationEmitter/SampleInvalidationEmitter.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {CBAE0FB2-0706-456F-8CF8-6F548F2E4161} 8 | Exe 9 | Properties 10 | SampleInvalidationEmitter 11 | SampleInvalidationEmitter 12 | v4.5 13 | 512 14 | true 15 | ..\..\ 16 | true 17 | 18 | 19 | 20 | AnyCPU 21 | true 22 | full 23 | false 24 | bin\Debug\ 25 | DEBUG;TRACE 26 | prompt 27 | 4 28 | 29 | 30 | AnyCPU 31 | pdbonly 32 | true 33 | bin\Release\ 34 | TRACE 35 | prompt 36 | 4 37 | 38 | 39 | 40 | ..\..\packages\StackExchange.Redis.1.0.481\lib\net45\StackExchange.Redis.dll 41 | True 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {13467724-727b-44ca-be9d-19bfc4599b34} 59 | RedisMemoryCacheInvalidation 60 | 61 | 62 | 63 | 70 | -------------------------------------------------------------------------------- /samples/SampleInvalidationEmitter/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/App_Start/BundleConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using System.Web.Optimization; 3 | 4 | namespace SampleWebApplication 5 | { 6 | public class BundleConfig 7 | { 8 | // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862 9 | public static void RegisterBundles(BundleCollection bundles) 10 | { 11 | bundles.Add(new ScriptBundle("~/bundles/jquery").Include( 12 | "~/Scripts/jquery-{version}.js")); 13 | 14 | bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( 15 | "~/Scripts/jquery.validate*")); 16 | 17 | // Use the development version of Modernizr to develop with and learn from. Then, when you're 18 | // ready for production, use the build tool at http://modernizr.com to pick only the tests you need. 19 | bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( 20 | "~/Scripts/modernizr-*")); 21 | 22 | bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( 23 | "~/Scripts/bootstrap.js", 24 | "~/Scripts/respond.js")); 25 | 26 | bundles.Add(new StyleBundle("~/Content/css").Include( 27 | "~/Content/bootstrap.css", 28 | "~/Content/site.css")); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/App_Start/FilterConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Web; 2 | using System.Web.Mvc; 3 | 4 | namespace SampleWebApplication 5 | { 6 | public class FilterConfig 7 | { 8 | public static void RegisterGlobalFilters(GlobalFilterCollection filters) 9 | { 10 | filters.Add(new HandleErrorAttribute()); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/App_Start/IdentityConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Entity; 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | using System.Web; 8 | using Microsoft.AspNet.Identity; 9 | using Microsoft.AspNet.Identity.EntityFramework; 10 | using Microsoft.AspNet.Identity.Owin; 11 | using Microsoft.Owin; 12 | using Microsoft.Owin.Security; 13 | using SampleWebApplication.Models; 14 | 15 | namespace SampleWebApplication 16 | { 17 | public class EmailService : IIdentityMessageService 18 | { 19 | public Task SendAsync(IdentityMessage message) 20 | { 21 | // Plug in your email service here to send an email. 22 | return Task.FromResult(0); 23 | } 24 | } 25 | 26 | public class SmsService : IIdentityMessageService 27 | { 28 | public Task SendAsync(IdentityMessage message) 29 | { 30 | // Plug in your SMS service here to send a text message. 31 | return Task.FromResult(0); 32 | } 33 | } 34 | 35 | // Configure the application user manager used in this application. UserManager is defined in ASP.NET Identity and is used by the application. 36 | public class ApplicationUserManager : UserManager 37 | { 38 | public ApplicationUserManager(IUserStore store) 39 | : base(store) 40 | { 41 | } 42 | 43 | public static ApplicationUserManager Create(IdentityFactoryOptions options, IOwinContext context) 44 | { 45 | var manager = new ApplicationUserManager(new UserStore(context.Get())); 46 | // Configure validation logic for usernames 47 | manager.UserValidator = new UserValidator(manager) 48 | { 49 | AllowOnlyAlphanumericUserNames = false, 50 | RequireUniqueEmail = true 51 | }; 52 | 53 | // Configure validation logic for passwords 54 | manager.PasswordValidator = new PasswordValidator 55 | { 56 | RequiredLength = 6, 57 | RequireNonLetterOrDigit = true, 58 | RequireDigit = true, 59 | RequireLowercase = true, 60 | RequireUppercase = true, 61 | }; 62 | 63 | // Configure user lockout defaults 64 | manager.UserLockoutEnabledByDefault = true; 65 | manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); 66 | manager.MaxFailedAccessAttemptsBeforeLockout = 5; 67 | 68 | // Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user 69 | // You can write your own provider and plug it in here. 70 | manager.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider 71 | { 72 | MessageFormat = "Your security code is {0}" 73 | }); 74 | manager.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider 75 | { 76 | Subject = "Security Code", 77 | BodyFormat = "Your security code is {0}" 78 | }); 79 | manager.EmailService = new EmailService(); 80 | manager.SmsService = new SmsService(); 81 | var dataProtectionProvider = options.DataProtectionProvider; 82 | if (dataProtectionProvider != null) 83 | { 84 | manager.UserTokenProvider = 85 | new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); 86 | } 87 | return manager; 88 | } 89 | } 90 | 91 | // Configure the application sign-in manager which is used in this application. 92 | public class ApplicationSignInManager : SignInManager 93 | { 94 | public ApplicationSignInManager(ApplicationUserManager userManager, IAuthenticationManager authenticationManager) 95 | : base(userManager, authenticationManager) 96 | { 97 | } 98 | 99 | public override Task CreateUserIdentityAsync(ApplicationUser user) 100 | { 101 | return user.GenerateUserIdentityAsync((ApplicationUserManager)UserManager); 102 | } 103 | 104 | public static ApplicationSignInManager Create(IdentityFactoryOptions options, IOwinContext context) 105 | { 106 | return new ApplicationSignInManager(context.GetUserManager(), context.Authentication); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/App_Start/RouteConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | using System.Web.Routing; 7 | 8 | namespace SampleWebApplication 9 | { 10 | public class RouteConfig 11 | { 12 | public static void RegisterRoutes(RouteCollection routes) 13 | { 14 | routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 15 | 16 | routes.MapRoute( 17 | name: "Default", 18 | url: "{controller}/{action}/{id}", 19 | defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 20 | ); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/App_Start/Startup.Auth.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNet.Identity; 3 | using Microsoft.AspNet.Identity.Owin; 4 | using Microsoft.Owin; 5 | using Microsoft.Owin.Security.Cookies; 6 | using Microsoft.Owin.Security.Google; 7 | using Owin; 8 | using SampleWebApplication.Models; 9 | 10 | namespace SampleWebApplication 11 | { 12 | public partial class Startup 13 | { 14 | // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864 15 | public void ConfigureAuth(IAppBuilder app) 16 | { 17 | // Configure the db context, user manager and signin manager to use a single instance per request 18 | app.CreatePerOwinContext(ApplicationDbContext.Create); 19 | app.CreatePerOwinContext(ApplicationUserManager.Create); 20 | app.CreatePerOwinContext(ApplicationSignInManager.Create); 21 | 22 | // Enable the application to use a cookie to store information for the signed in user 23 | // and to use a cookie to temporarily store information about a user logging in with a third party login provider 24 | // Configure the sign in cookie 25 | app.UseCookieAuthentication(new CookieAuthenticationOptions 26 | { 27 | AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, 28 | LoginPath = new PathString("/Account/Login"), 29 | Provider = new CookieAuthenticationProvider 30 | { 31 | // Enables the application to validate the security stamp when the user logs in. 32 | // This is a security feature which is used when you change a password or add an external login to your account. 33 | OnValidateIdentity = SecurityStampValidator.OnValidateIdentity( 34 | validateInterval: TimeSpan.FromMinutes(30), 35 | regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) 36 | } 37 | }); 38 | app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); 39 | 40 | // Enables the application to temporarily store user information when they are verifying the second factor in the two-factor authentication process. 41 | app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5)); 42 | 43 | // Enables the application to remember the second login verification factor such as phone or email. 44 | // Once you check this option, your second step of verification during the login process will be remembered on the device where you logged in from. 45 | // This is similar to the RememberMe option when you log in. 46 | app.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); 47 | 48 | // Uncomment the following lines to enable logging in with third party login providers 49 | //app.UseMicrosoftAccountAuthentication( 50 | // clientId: "", 51 | // clientSecret: ""); 52 | 53 | //app.UseTwitterAuthentication( 54 | // consumerKey: "", 55 | // consumerSecret: ""); 56 | 57 | //app.UseFacebookAuthentication( 58 | // appId: "", 59 | // appSecret: ""); 60 | 61 | //app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions() 62 | //{ 63 | // ClientId = "", 64 | // ClientSecret = "" 65 | //}); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /samples/SampleWebApplication/Content/Site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Set padding to keep content from hitting the edges */ 7 | .body-content { 8 | padding-left: 15px; 9 | padding-right: 15px; 10 | } 11 | 12 | /* Override the default bootstrap behavior where horizontal description lists 13 | will truncate terms that are too long to fit in the left column 14 | */ 15 | .dl-horizontal dt { 16 | white-space: normal; 17 | } 18 | 19 | /* Set width on the form input elements since they're 100% wide by default */ 20 | input, 21 | select, 22 | textarea { 23 | max-width: 280px; 24 | } 25 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.Caching; 6 | using System.Web; 7 | using System.Web.Mvc; 8 | 9 | namespace SampleWebApplication.Controllers 10 | { 11 | public class HomeController : Controller 12 | { 13 | private const string parentkey = "mynotifmessage"; 14 | 15 | public ActionResult Index() 16 | { 17 | var cache = MemoryCache.Default; 18 | 19 | var cacheItem = new CacheItem("onekey", DateTime.Now); 20 | if (!cache.Contains(cacheItem.Key)) 21 | { 22 | var policy = new CacheItemPolicy(); 23 | policy.ChangeMonitors.Add(InvalidationManager.CreateChangeMonitor(parentkey)); 24 | policy.AbsoluteExpiration = DateTime.Now.AddYears(1); // just to create not expirable item 25 | MemoryCache.Default.Add(cacheItem, policy); 26 | } 27 | 28 | 29 | DateTime dt = (DateTime)cache[cacheItem.Key]; 30 | 31 | ViewBag.Message = string.Format("'{0}' was set at {1}", cacheItem.Key, dt.ToLongTimeString()); 32 | 33 | return View(); 34 | } 35 | 36 | public ActionResult Invalidate() 37 | { 38 | InvalidationManager.InvalidateAsync(parentkey); 39 | 40 | return RedirectToAction("Index"); 41 | } 42 | 43 | public ActionResult About() 44 | { 45 | ViewBag.Message = "Your application description page."; 46 | 47 | return View(); 48 | } 49 | 50 | public ActionResult Contact() 51 | { 52 | ViewBag.Message = "Your contact page."; 53 | 54 | return View(); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /samples/SampleWebApplication/Global.asax: -------------------------------------------------------------------------------- 1 | <%@ Application Codebehind="Global.asax.cs" Inherits="SampleWebApplication.MvcApplication" Language="C#" %> 2 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Global.asax.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Runtime.Caching; 6 | using System.Web; 7 | using System.Web.Mvc; 8 | using System.Web.Optimization; 9 | using System.Web.Routing; 10 | 11 | namespace SampleWebApplication 12 | { 13 | public class MvcApplication : System.Web.HttpApplication 14 | { 15 | protected void Application_Start() 16 | { 17 | AreaRegistration.RegisterAllAreas(); 18 | FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); 19 | RouteConfig.RegisterRoutes(RouteTable.Routes); 20 | BundleConfig.RegisterBundles(BundleTable.Bundles); 21 | 22 | //add somewhere else 23 | InvalidationManager.Configure("localhost:6379", new InvalidationSettings()); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Models/AccountViewModels.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace SampleWebApplication.Models 5 | { 6 | public class ExternalLoginConfirmationViewModel 7 | { 8 | [Required] 9 | [Display(Name = "Email")] 10 | public string Email { get; set; } 11 | } 12 | 13 | public class ExternalLoginListViewModel 14 | { 15 | public string ReturnUrl { get; set; } 16 | } 17 | 18 | public class SendCodeViewModel 19 | { 20 | public string SelectedProvider { get; set; } 21 | public ICollection Providers { get; set; } 22 | public string ReturnUrl { get; set; } 23 | public bool RememberMe { get; set; } 24 | } 25 | 26 | public class VerifyCodeViewModel 27 | { 28 | [Required] 29 | public string Provider { get; set; } 30 | 31 | [Required] 32 | [Display(Name = "Code")] 33 | public string Code { get; set; } 34 | public string ReturnUrl { get; set; } 35 | 36 | [Display(Name = "Remember this browser?")] 37 | public bool RememberBrowser { get; set; } 38 | 39 | public bool RememberMe { get; set; } 40 | } 41 | 42 | public class ForgotViewModel 43 | { 44 | [Required] 45 | [Display(Name = "Email")] 46 | public string Email { get; set; } 47 | } 48 | 49 | public class LoginViewModel 50 | { 51 | [Required] 52 | [Display(Name = "Email")] 53 | [EmailAddress] 54 | public string Email { get; set; } 55 | 56 | [Required] 57 | [DataType(DataType.Password)] 58 | [Display(Name = "Password")] 59 | public string Password { get; set; } 60 | 61 | [Display(Name = "Remember me?")] 62 | public bool RememberMe { get; set; } 63 | } 64 | 65 | public class RegisterViewModel 66 | { 67 | [Required] 68 | [EmailAddress] 69 | [Display(Name = "Email")] 70 | public string Email { get; set; } 71 | 72 | [Required] 73 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 74 | [DataType(DataType.Password)] 75 | [Display(Name = "Password")] 76 | public string Password { get; set; } 77 | 78 | [DataType(DataType.Password)] 79 | [Display(Name = "Confirm password")] 80 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 81 | public string ConfirmPassword { get; set; } 82 | } 83 | 84 | public class ResetPasswordViewModel 85 | { 86 | [Required] 87 | [EmailAddress] 88 | [Display(Name = "Email")] 89 | public string Email { get; set; } 90 | 91 | [Required] 92 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 93 | [DataType(DataType.Password)] 94 | [Display(Name = "Password")] 95 | public string Password { get; set; } 96 | 97 | [DataType(DataType.Password)] 98 | [Display(Name = "Confirm password")] 99 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 100 | public string ConfirmPassword { get; set; } 101 | 102 | public string Code { get; set; } 103 | } 104 | 105 | public class ForgotPasswordViewModel 106 | { 107 | [Required] 108 | [EmailAddress] 109 | [Display(Name = "Email")] 110 | public string Email { get; set; } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Models/IdentityModels.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Entity; 2 | using System.Security.Claims; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNet.Identity; 5 | using Microsoft.AspNet.Identity.EntityFramework; 6 | 7 | namespace SampleWebApplication.Models 8 | { 9 | // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more. 10 | public class ApplicationUser : IdentityUser 11 | { 12 | public async Task GenerateUserIdentityAsync(UserManager manager) 13 | { 14 | // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType 15 | var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); 16 | // Add custom user claims here 17 | return userIdentity; 18 | } 19 | } 20 | 21 | public class ApplicationDbContext : IdentityDbContext 22 | { 23 | public ApplicationDbContext() 24 | : base("DefaultConnection", throwIfV1Schema: false) 25 | { 26 | } 27 | 28 | public static ApplicationDbContext Create() 29 | { 30 | return new ApplicationDbContext(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /samples/SampleWebApplication/Models/ManageViewModels.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using Microsoft.AspNet.Identity; 4 | using Microsoft.Owin.Security; 5 | 6 | namespace SampleWebApplication.Models 7 | { 8 | public class IndexViewModel 9 | { 10 | public bool HasPassword { get; set; } 11 | public IList Logins { get; set; } 12 | public string PhoneNumber { get; set; } 13 | public bool TwoFactor { get; set; } 14 | public bool BrowserRemembered { get; set; } 15 | } 16 | 17 | public class ManageLoginsViewModel 18 | { 19 | public IList CurrentLogins { get; set; } 20 | public IList OtherLogins { get; set; } 21 | } 22 | 23 | public class FactorViewModel 24 | { 25 | public string Purpose { get; set; } 26 | } 27 | 28 | public class SetPasswordViewModel 29 | { 30 | [Required] 31 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 32 | [DataType(DataType.Password)] 33 | [Display(Name = "New password")] 34 | public string NewPassword { get; set; } 35 | 36 | [DataType(DataType.Password)] 37 | [Display(Name = "Confirm new password")] 38 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 39 | public string ConfirmPassword { get; set; } 40 | } 41 | 42 | public class ChangePasswordViewModel 43 | { 44 | [Required] 45 | [DataType(DataType.Password)] 46 | [Display(Name = "Current password")] 47 | public string OldPassword { get; set; } 48 | 49 | [Required] 50 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 51 | [DataType(DataType.Password)] 52 | [Display(Name = "New password")] 53 | public string NewPassword { get; set; } 54 | 55 | [DataType(DataType.Password)] 56 | [Display(Name = "Confirm new password")] 57 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 58 | public string ConfirmPassword { get; set; } 59 | } 60 | 61 | public class AddPhoneNumberViewModel 62 | { 63 | [Required] 64 | [Phone] 65 | [Display(Name = "Phone Number")] 66 | public string Number { get; set; } 67 | } 68 | 69 | public class VerifyPhoneNumberViewModel 70 | { 71 | [Required] 72 | [Display(Name = "Code")] 73 | public string Code { get; set; } 74 | 75 | [Required] 76 | [Phone] 77 | [Display(Name = "Phone Number")] 78 | public string PhoneNumber { get; set; } 79 | } 80 | 81 | public class ConfigureTwoFactorViewModel 82 | { 83 | public string SelectedProvider { get; set; } 84 | public ICollection Providers { get; set; } 85 | } 86 | } -------------------------------------------------------------------------------- /samples/SampleWebApplication/Project_Readme.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Your ASP.NET application 6 | 95 | 96 | 97 | 98 | 102 | 103 |
104 |
105 |

This application consists of:

106 |
    107 |
  • Sample pages showing basic nav between Home, About, and Contact
  • 108 |
  • Theming using Bootstrap
  • 109 |
  • Authentication, if selected, shows how to register and sign in
  • 110 |
  • ASP.NET features managed using NuGet
  • 111 |
112 |
113 | 114 | 131 | 132 |
133 |

Deploy

134 | 139 |
140 | 141 |
142 |

Get help

143 | 147 |
148 |
149 | 150 | 151 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("SampleWebApplication")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("SampleWebApplication")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b3c5c162-6274-41d5-8d9a-0d5c1d22d6e0")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Revision and Build Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.14474.12")] 35 | [assembly: AssemblyFileVersion("1.0.14474.12")] 36 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Scripts/_references.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/samples/SampleWebApplication/Scripts/_references.js -------------------------------------------------------------------------------- /samples/SampleWebApplication/Scripts/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | /* NUGET: BEGIN LICENSE TEXT 2 | * 3 | * Microsoft grants you the right to use these script files for the sole 4 | * purpose of either: (i) interacting through your browser with the Microsoft 5 | * website or online service, subject to the applicable licensing or use 6 | * terms; or (ii) using the files as included with a Microsoft product subject 7 | * to that product's license terms. Microsoft reserves all other rights to the 8 | * files not expressly granted by Microsoft, whether by implication, estoppel 9 | * or otherwise. Insofar as a script file is dual licensed under GPL, 10 | * Microsoft neither took the code under GPL nor distributes it thereunder but 11 | * under the terms set out in this paragraph. All notices and licenses 12 | * below are for informational purposes only. 13 | * 14 | * NUGET: END LICENSE TEXT */ 15 | /* 16 | ** Unobtrusive validation support library for jQuery and jQuery Validate 17 | ** Copyright (C) Microsoft Corporation. All rights reserved. 18 | */ 19 | (function(a){var d=a.validator,b,e="unobtrusiveValidation";function c(a,b,c){a.rules[b]=c;if(a.message)a.messages[b]=a.message}function j(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function f(a){return a.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function h(a){return a.substr(0,a.lastIndexOf(".")+1)}function g(a,b){if(a.indexOf("*.")===0)a=a.replace("*.",b);return a}function m(c,e){var b=a(this).find("[data-valmsg-for='"+f(e[0].name)+"']"),d=b.attr("data-valmsg-replace"),g=d?a.parseJSON(d)!==false:null;b.removeClass("field-validation-valid").addClass("field-validation-error");c.data("unobtrusiveContainer",b);if(g){b.empty();c.removeClass("input-validation-error").appendTo(b)}else c.hide()}function l(e,d){var c=a(this).find("[data-valmsg-summary=true]"),b=c.find("ul");if(b&&b.length&&d.errorList.length){b.empty();c.addClass("validation-summary-errors").removeClass("validation-summary-valid");a.each(d.errorList,function(){a("
  • ").html(this.message).appendTo(b)})}}function k(d){var b=d.data("unobtrusiveContainer"),c=b.attr("data-valmsg-replace"),e=c?a.parseJSON(c):null;if(b){b.addClass("field-validation-valid").removeClass("field-validation-error");d.removeData("unobtrusiveContainer");e&&b.empty()}}function n(){var b=a(this),c="__jquery_unobtrusive_validation_form_reset";if(b.data(c))return;b.data(c,true);try{b.data("validator").resetForm()}finally{b.removeData(c)}b.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors");b.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}function i(b){var c=a(b),f=c.data(e),i=a.proxy(n,b),g=d.unobtrusive.options||{},h=function(e,d){var c=g[e];c&&a.isFunction(c)&&c.apply(b,d)};if(!f){f={options:{errorClass:g.errorClass||"input-validation-error",errorElement:g.errorElement||"span",errorPlacement:function(){m.apply(b,arguments);h("errorPlacement",arguments)},invalidHandler:function(){l.apply(b,arguments);h("invalidHandler",arguments)},messages:{},rules:{},success:function(){k.apply(b,arguments);h("success",arguments)}},attachValidation:function(){c.off("reset."+e,i).on("reset."+e,i).validate(this.options)},validate:function(){c.validate();return c.valid()}};c.data(e,f)}return f}d.unobtrusive={adapters:[],parseElement:function(b,h){var d=a(b),f=d.parents("form")[0],c,e,g;if(!f)return;c=i(f);c.options.rules[b.name]=e={};c.options.messages[b.name]=g={};a.each(this.adapters,function(){var c="data-val-"+this.name,i=d.attr(c),h={};if(i!==undefined){c+="-";a.each(this.params,function(){h[this]=d.attr(c+this)});this.adapt({element:b,form:f,message:i,params:h,rules:e,messages:g})}});a.extend(e,{__dummy__:true});!h&&c.attachValidation()},parse:function(c){var b=a(c),e=b.parents().addBack().filter("form").add(b.find("form")).has("[data-val=true]");b.find("[data-val=true]").each(function(){d.unobtrusive.parseElement(this,true)});e.each(function(){var a=i(this);a&&a.attachValidation()})}};b=d.unobtrusive.adapters;b.add=function(c,a,b){if(!b){b=a;a=[]}this.push({name:c,params:a,adapt:b});return this};b.addBool=function(a,b){return this.add(a,function(d){c(d,b||a,true)})};b.addMinMax=function(e,g,f,a,d,b){return this.add(e,[d||"min",b||"max"],function(b){var e=b.params.min,d=b.params.max;if(e&&d)c(b,a,[e,d]);else if(e)c(b,g,e);else d&&c(b,f,d)})};b.addSingleVal=function(a,b,d){return this.add(a,[b||"val"],function(e){c(e,d||a,e.params[b])})};d.addMethod("__dummy__",function(){return true});d.addMethod("regex",function(b,c,d){var a;if(this.optional(c))return true;a=(new RegExp(d)).exec(b);return a&&a.index===0&&a[0].length===b.length});d.addMethod("nonalphamin",function(c,d,b){var a;if(b){a=c.match(/\W/g);a=a&&a.length>=b}return a});if(d.methods.extension){b.addSingleVal("accept","mimtype");b.addSingleVal("extension","extension")}else b.addSingleVal("extension","extension","accept");b.addSingleVal("regex","pattern");b.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url");b.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range");b.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength");b.add("equalto",["other"],function(b){var i=h(b.element.name),j=b.params.other,d=g(j,i),e=a(b.form).find(":input").filter("[name='"+f(d)+"']")[0];c(b,"equalTo",e)});b.add("required",function(a){(a.element.tagName.toUpperCase()!=="INPUT"||a.element.type.toUpperCase()!=="CHECKBOX")&&c(a,"required",true)});b.add("remote",["url","type","additionalfields"],function(b){var d={url:b.params.url,type:b.params.type||"GET",data:{}},e=h(b.element.name);a.each(j(b.params.additionalfields||b.element.name),function(i,h){var c=g(h,e);d.data[c]=function(){var d=a(b.form).find(":input").filter("[name='"+f(c)+"']");return d.is(":checkbox")?d.filter(":checked").val()||d.filter(":hidden").val()||"":d.is(":radio")?d.filter(":checked").val()||"":d.val()}});c(b,"remote",d)});b.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&c(a,"minlength",a.params.min);a.params.nonalphamin&&c(a,"nonalphamin",a.params.nonalphamin);a.params.regex&&c(a,"regex",a.params.regex)});a(function(){d.unobtrusive.parse(document)})})(jQuery); -------------------------------------------------------------------------------- /samples/SampleWebApplication/Scripts/respond.matchmedia.addListener.min.js: -------------------------------------------------------------------------------- 1 | /*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl 2 | * Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT 3 | * */ 4 | 5 | !function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";if(a.matchMedia&&a.matchMedia("all").addListener)return!1;var b=a.matchMedia,c=b("only all").matches,d=!1,e=0,f=[],g=function(){a.clearTimeout(e),e=a.setTimeout(function(){for(var c=0,d=f.length;d>c;c++){var e=f[c].mql,g=f[c].listeners||[],h=b(e.media).matches;if(h!==e.matches){e.matches=h;for(var i=0,j=g.length;j>i;i++)g[i].call(a,e)}}},30)};a.matchMedia=function(e){var h=b(e),i=[],j=0;return h.addListener=function(b){c&&(d||(d=!0,a.addEventListener("resize",g,!0)),0===j&&(j=f.push({mql:h,listeners:i})),i.push(b))},h.removeListener=function(a){for(var b=0,c=i.length;c>b;b++)i[b]===a&&i.splice(b,1)},h}}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b #mq-test-1 { width: 42px; }',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b@ViewBag.Title. 6 |
    7 |

    8 | Thank you for confirming your email. Please @Html.ActionLink("Click here to Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) 9 |

    10 |
    11 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/ExternalLoginConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.ExternalLoginConfirmationViewModel 2 | @{ 3 | ViewBag.Title = "Register"; 4 | } 5 |

    @ViewBag.Title.

    6 |

    Associate your @ViewBag.LoginProvider account.

    7 | 8 | @using (Html.BeginForm("ExternalLoginConfirmation", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 | 12 |

    Association Form

    13 |
    14 | @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 15 |

    16 | You've successfully authenticated with @ViewBag.LoginProvider. 17 | Please enter a user name for this site below and click the Register button to finish 18 | logging in. 19 |

    20 |
    21 | @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) 22 |
    23 | @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) 24 | @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" }) 25 |
    26 |
    27 |
    28 |
    29 | 30 |
    31 |
    32 | } 33 | 34 | @section Scripts { 35 | @Scripts.Render("~/bundles/jqueryval") 36 | } 37 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/ExternalLoginFailure.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Login Failure"; 3 | } 4 | 5 |
    6 |

    @ViewBag.Title.

    7 |

    Unsuccessful login with service.

    8 |
    9 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/ForgotPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.ForgotPasswordViewModel 2 | @{ 3 | ViewBag.Title = "Forgot your password?"; 4 | } 5 | 6 |

    @ViewBag.Title.

    7 | 8 | @using (Html.BeginForm("ForgotPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 |

    Enter your email.

    12 |
    13 | @Html.ValidationSummary("", new { @class = "text-danger" }) 14 |
    15 | @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) 16 |
    17 | @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) 18 |
    19 |
    20 |
    21 |
    22 | 23 |
    24 |
    25 | } 26 | 27 | @section Scripts { 28 | @Scripts.Render("~/bundles/jqueryval") 29 | } 30 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/ForgotPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Forgot Password Confirmation"; 3 | } 4 | 5 |
    6 |

    @ViewBag.Title.

    7 |
    8 |
    9 |

    10 | Please check your email to reset your password. 11 |

    12 |
    13 | 14 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @using SampleWebApplication.Models 2 | @model LoginViewModel 3 | @{ 4 | ViewBag.Title = "Log in"; 5 | } 6 | 7 |

    @ViewBag.Title.

    8 |
    9 |
    10 |
    11 | @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 12 | { 13 | @Html.AntiForgeryToken() 14 |

    Use a local account to log in.

    15 |
    16 | @Html.ValidationSummary(true, "", new { @class = "text-danger" }) 17 |
    18 | @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) 19 |
    20 | @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) 21 | @Html.ValidationMessageFor(m => m.Email, "", new { @class = "text-danger" }) 22 |
    23 |
    24 |
    25 | @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) 26 |
    27 | @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) 28 | @Html.ValidationMessageFor(m => m.Password, "", new { @class = "text-danger" }) 29 |
    30 |
    31 |
    32 |
    33 |
    34 | @Html.CheckBoxFor(m => m.RememberMe) 35 | @Html.LabelFor(m => m.RememberMe) 36 |
    37 |
    38 |
    39 |
    40 |
    41 | 42 |
    43 |
    44 |

    45 | @Html.ActionLink("Register as a new user", "Register") 46 |

    47 | @* Enable this once you have account confirmation enabled for password reset functionality 48 |

    49 | @Html.ActionLink("Forgot your password?", "ForgotPassword") 50 |

    *@ 51 | } 52 |
    53 |
    54 |
    55 |
    56 | @Html.Partial("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl }) 57 |
    58 |
    59 |
    60 | 61 | @section Scripts { 62 | @Scripts.Render("~/bundles/jqueryval") 63 | } -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/Register.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.RegisterViewModel 2 | @{ 3 | ViewBag.Title = "Register"; 4 | } 5 | 6 |

    @ViewBag.Title.

    7 | 8 | @using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 |

    Create a new account.

    12 |
    13 | @Html.ValidationSummary("", new { @class = "text-danger" }) 14 |
    15 | @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) 16 |
    17 | @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) 18 |
    19 |
    20 |
    21 | @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) 22 |
    23 | @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) 24 |
    25 |
    26 |
    27 | @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) 28 |
    29 | @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) 30 |
    31 |
    32 |
    33 |
    34 | 35 |
    36 |
    37 | } 38 | 39 | @section Scripts { 40 | @Scripts.Render("~/bundles/jqueryval") 41 | } 42 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/ResetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.ResetPasswordViewModel 2 | @{ 3 | ViewBag.Title = "Reset password"; 4 | } 5 | 6 |

    @ViewBag.Title.

    7 | 8 | @using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 |

    Reset your password.

    12 |
    13 | @Html.ValidationSummary("", new { @class = "text-danger" }) 14 | @Html.HiddenFor(model => model.Code) 15 |
    16 | @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" }) 17 |
    18 | @Html.TextBoxFor(m => m.Email, new { @class = "form-control" }) 19 |
    20 |
    21 |
    22 | @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) 23 |
    24 | @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) 25 |
    26 |
    27 |
    28 | @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) 29 |
    30 | @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) 31 |
    32 |
    33 |
    34 |
    35 | 36 |
    37 |
    38 | } 39 | 40 | @section Scripts { 41 | @Scripts.Render("~/bundles/jqueryval") 42 | } 43 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/ResetPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Reset password confirmation"; 3 | } 4 | 5 |
    6 |

    @ViewBag.Title.

    7 |
    8 |
    9 |

    10 | Your password has been reset. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" }) 11 |

    12 |
    13 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/SendCode.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.SendCodeViewModel 2 | @{ 3 | ViewBag.Title = "Send"; 4 | } 5 | 6 |

    @ViewBag.Title.

    7 | 8 | @using (Html.BeginForm("SendCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { 9 | @Html.AntiForgeryToken() 10 | @Html.Hidden("rememberMe", @Model.RememberMe) 11 |

    Send verification code

    12 |
    13 |
    14 |
    15 | Select Two-Factor Authentication Provider: 16 | @Html.DropDownListFor(model => model.SelectedProvider, Model.Providers) 17 | 18 |
    19 |
    20 | } 21 | 22 | @section Scripts { 23 | @Scripts.Render("~/bundles/jqueryval") 24 | } 25 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/VerifyCode.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.VerifyCodeViewModel 2 | @{ 3 | ViewBag.Title = "Verify"; 4 | } 5 | 6 |

    @ViewBag.Title.

    7 | 8 | @using (Html.BeginForm("VerifyCode", "Account", new { ReturnUrl = Model.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" })) { 9 | @Html.AntiForgeryToken() 10 | @Html.Hidden("provider", @Model.Provider) 11 | @Html.Hidden("rememberMe", @Model.RememberMe) 12 |

    Enter verification code

    13 |
    14 | @Html.ValidationSummary("", new { @class = "text-danger" }) 15 |
    16 | @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" }) 17 |
    18 | @Html.TextBoxFor(m => m.Code, new { @class = "form-control" }) 19 |
    20 |
    21 |
    22 |
    23 |
    24 | @Html.CheckBoxFor(m => m.RememberBrowser) 25 | @Html.LabelFor(m => m.RememberBrowser) 26 |
    27 |
    28 |
    29 |
    30 |
    31 | 32 |
    33 |
    34 | } 35 | 36 | @section Scripts { 37 | @Scripts.Render("~/bundles/jqueryval") 38 | } 39 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Account/_ExternalLoginsListPartial.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.ExternalLoginListViewModel 2 | @using Microsoft.Owin.Security 3 | 4 |

    Use another service to log in.

    5 |
    6 | @{ 7 | var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes(); 8 | if (loginProviders.Count() == 0) { 9 |
    10 |

    11 | There are no external authentication services configured. See this article 12 | for details on setting up this ASP.NET application to support logging in via external services. 13 |

    14 |
    15 | } 16 | else { 17 | using (Html.BeginForm("ExternalLogin", "Account", new { ReturnUrl = Model.ReturnUrl })) { 18 | @Html.AntiForgeryToken() 19 |
    20 |

    21 | @foreach (AuthenticationDescription p in loginProviders) { 22 | 23 | } 24 |

    25 |
    26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Home/About.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "About"; 3 | } 4 |

    @ViewBag.Title.

    5 |

    @ViewBag.Message

    6 | 7 |

    Use this area to provide additional information.

    8 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Home/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Contact"; 3 | } 4 |

    @ViewBag.Title.

    5 |

    @ViewBag.Message

    6 | 7 |
    8 | One Microsoft Way
    9 | Redmond, WA 98052-6399
    10 | P: 11 | 425.555.0100 12 |
    13 | 14 |
    15 | Support: Support@example.com
    16 | Marketing: Marketing@example.com 17 |
    -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewBag.Title = "Home Page"; 3 | } 4 | 5 |
    6 |

    @ViewBag.Message

    7 |
    8 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Manage/AddPhoneNumber.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.AddPhoneNumberViewModel 2 | @{ 3 | ViewBag.Title = "Phone Number"; 4 | } 5 | 6 |

    @ViewBag.Title.

    7 | 8 | @using (Html.BeginForm("AddPhoneNumber", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 |

    Add a phone number

    12 |
    13 | @Html.ValidationSummary("", new { @class = "text-danger" }) 14 |
    15 | @Html.LabelFor(m => m.Number, new { @class = "col-md-2 control-label" }) 16 |
    17 | @Html.TextBoxFor(m => m.Number, new { @class = "form-control" }) 18 |
    19 |
    20 |
    21 |
    22 | 23 |
    24 |
    25 | } 26 | 27 | @section Scripts { 28 | @Scripts.Render("~/bundles/jqueryval") 29 | } 30 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Manage/ChangePassword.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.ChangePasswordViewModel 2 | @{ 3 | ViewBag.Title = "Change Password"; 4 | } 5 | 6 |

    @ViewBag.Title.

    7 | 8 | @using (Html.BeginForm("ChangePassword", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 |

    Change Password Form

    12 |
    13 | @Html.ValidationSummary("", new { @class = "text-danger" }) 14 |
    15 | @Html.LabelFor(m => m.OldPassword, new { @class = "col-md-2 control-label" }) 16 |
    17 | @Html.PasswordFor(m => m.OldPassword, new { @class = "form-control" }) 18 |
    19 |
    20 |
    21 | @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" }) 22 |
    23 | @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" }) 24 |
    25 |
    26 |
    27 | @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) 28 |
    29 | @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) 30 |
    31 |
    32 |
    33 |
    34 | 35 |
    36 |
    37 | } 38 | @section Scripts { 39 | @Scripts.Render("~/bundles/jqueryval") 40 | } -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Manage/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.IndexViewModel 2 | @{ 3 | ViewBag.Title = "Manage"; 4 | } 5 | 6 |

    @ViewBag.Title.

    7 | 8 |

    @ViewBag.StatusMessage

    9 |
    10 |

    Change your account settings

    11 |
    12 |
    13 |
    Password:
    14 |
    15 | [ 16 | @if (Model.HasPassword) 17 | { 18 | @Html.ActionLink("Change your password", "ChangePassword") 19 | } 20 | else 21 | { 22 | @Html.ActionLink("Create", "SetPassword") 23 | } 24 | ] 25 |
    26 |
    External Logins:
    27 |
    28 | @Model.Logins.Count [ 29 | @Html.ActionLink("Manage", "ManageLogins") ] 30 |
    31 | @* 32 | Phone Numbers can used as a second factor of verification in a two-factor authentication system. 33 | 34 | See this article 35 | for details on setting up this ASP.NET application to support two-factor authentication using SMS. 36 | 37 | Uncomment the following block after you have set up two-factor authentication 38 | *@ 39 | @* 40 |
    Phone Number:
    41 |
    42 | @(Model.PhoneNumber ?? "None") [ 43 | @if (Model.PhoneNumber != null) 44 | { 45 | @Html.ActionLink("Change", "AddPhoneNumber") 46 | @:  |  47 | @Html.ActionLink("Remove", "RemovePhoneNumber") 48 | } 49 | else 50 | { 51 | @Html.ActionLink("Add", "AddPhoneNumber") 52 | } 53 | ] 54 |
    55 | *@ 56 |
    Two-Factor Authentication:
    57 |
    58 |

    59 | There are no two-factor authentication providers configured. See this article 60 | for details on setting up this ASP.NET application to support two-factor authentication. 61 |

    62 | @*@if (Model.TwoFactor) 63 | { 64 | using (Html.BeginForm("DisableTwoFactorAuthentication", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 65 | { 66 | @Html.AntiForgeryToken() 67 | Enabled 68 | 69 | 70 | } 71 | } 72 | else 73 | { 74 | using (Html.BeginForm("EnableTwoFactorAuthentication", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 75 | { 76 | @Html.AntiForgeryToken() 77 | Disabled 78 | 79 | 80 | } 81 | }*@ 82 |
    83 |
    84 |
    85 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Manage/ManageLogins.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.ManageLoginsViewModel 2 | @using Microsoft.Owin.Security 3 | @{ 4 | ViewBag.Title = "Manage your external logins"; 5 | } 6 | 7 |

    @ViewBag.Title.

    8 | 9 |

    @ViewBag.StatusMessage

    10 | @{ 11 | var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes(); 12 | if (loginProviders.Count() == 0) { 13 |
    14 |

    15 | There are no external authentication services configured. See this article 16 | for details on setting up this ASP.NET application to support logging in via external services. 17 |

    18 |
    19 | } 20 | else 21 | { 22 | if (Model.CurrentLogins.Count > 0) 23 | { 24 |

    Registered Logins

    25 | 26 | 27 | @foreach (var account in Model.CurrentLogins) 28 | { 29 | 30 | 31 | 49 | 50 | } 51 | 52 |
    @account.LoginProvider 32 | @if (ViewBag.ShowRemoveButton) 33 | { 34 | using (Html.BeginForm("RemoveLogin", "Manage")) 35 | { 36 | @Html.AntiForgeryToken() 37 |
    38 | @Html.Hidden("loginProvider", account.LoginProvider) 39 | @Html.Hidden("providerKey", account.ProviderKey) 40 | 41 |
    42 | } 43 | } 44 | else 45 | { 46 | @:   47 | } 48 |
    53 | } 54 | if (Model.OtherLogins.Count > 0) 55 | { 56 | using (Html.BeginForm("LinkLogin", "Manage")) 57 | { 58 | @Html.AntiForgeryToken() 59 |
    60 |

    61 | @foreach (AuthenticationDescription p in Model.OtherLogins) 62 | { 63 | 64 | } 65 |

    66 |
    67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Manage/SetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.SetPasswordViewModel 2 | @{ 3 | ViewBag.Title = "Create Password"; 4 | } 5 | 6 |

    @ViewBag.Title.

    7 |

    8 | You do not have a local username/password for this site. Add a local 9 | account so you can log in without an external login. 10 |

    11 | 12 | @using (Html.BeginForm("SetPassword", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 13 | { 14 | @Html.AntiForgeryToken() 15 | 16 |

    Create Local Login

    17 |
    18 | @Html.ValidationSummary("", new { @class = "text-danger" }) 19 |
    20 | @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" }) 21 |
    22 | @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" }) 23 |
    24 |
    25 |
    26 | @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) 27 |
    28 | @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) 29 |
    30 |
    31 |
    32 |
    33 | 34 |
    35 |
    36 | } 37 | @section Scripts { 38 | @Scripts.Render("~/bundles/jqueryval") 39 | } -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Manage/VerifyPhoneNumber.cshtml: -------------------------------------------------------------------------------- 1 | @model SampleWebApplication.Models.VerifyPhoneNumberViewModel 2 | @{ 3 | ViewBag.Title = "Verify Phone Number"; 4 | } 5 | 6 |

    @ViewBag.Title.

    7 | 8 | @using (Html.BeginForm("VerifyPhoneNumber", "Manage", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) 9 | { 10 | @Html.AntiForgeryToken() 11 | @Html.Hidden("phoneNumber", @Model.PhoneNumber) 12 |

    Enter verification code

    13 |
    @ViewBag.Status
    14 |
    15 | @Html.ValidationSummary("", new { @class = "text-danger" }) 16 |
    17 | @Html.LabelFor(m => m.Code, new { @class = "col-md-2 control-label" }) 18 |
    19 | @Html.TextBoxFor(m => m.Code, new { @class = "form-control" }) 20 |
    21 |
    22 |
    23 |
    24 | 25 |
    26 |
    27 | } 28 | 29 | @section Scripts { 30 | @Scripts.Render("~/bundles/jqueryval") 31 | } 32 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model System.Web.Mvc.HandleErrorInfo 2 | 3 | @{ 4 | ViewBag.Title = "Error"; 5 | } 6 | 7 |

    Error.

    8 |

    An error occurred while processing your request.

    9 | 10 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Shared/Lockout.cshtml: -------------------------------------------------------------------------------- 1 | @model System.Web.Mvc.HandleErrorInfo 2 | 3 | @{ 4 | ViewBag.Title = "Locked Out"; 5 | } 6 | 7 |
    8 |

    Locked out.

    9 |

    This account has been locked out, please try again later.

    10 |
    11 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewBag.Title - My ASP.NET Application 7 | @Styles.Render("~/Content/css") 8 | @Scripts.Render("~/bundles/modernizr") 9 | 10 | 11 | 12 | 30 |
    31 | @RenderBody() 32 |
    33 |
    34 |

    © @DateTime.Now.Year - My ASP.NET Application

    35 |
    36 |
    37 | 38 | @Scripts.Render("~/bundles/jquery") 39 | @Scripts.Render("~/bundles/bootstrap") 40 | @RenderSection("scripts", required: false) 41 | 42 | 43 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Shared/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNet.Identity 2 | @if (Request.IsAuthenticated) 3 | { 4 | using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" })) 5 | { 6 | @Html.AntiForgeryToken() 7 | 8 | 14 | } 15 | } 16 | else 17 | { 18 | 22 | } 23 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/Web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 |
    7 |
    8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "~/Views/Shared/_Layout.cshtml"; 3 | } 4 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Web.Debug.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 17 | 18 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Web.Release.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/Web.config: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /samples/SampleWebApplication/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/samples/SampleWebApplication/favicon.ico -------------------------------------------------------------------------------- /samples/SampleWebApplication/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/samples/SampleWebApplication/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /samples/SampleWebApplication/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/samples/SampleWebApplication/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /samples/SampleWebApplication/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/samples/SampleWebApplication/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /samples/SampleWebApplication/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/samples/SampleWebApplication/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /samples/SampleWebApplication/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /scripts/pack.ps1: -------------------------------------------------------------------------------- 1 | $root = $env:APPVEYOR_BUILD_FOLDER 2 | $versionStr = $env:appveyor_build_version 3 | 4 | $content = (Get-Content $root\RedisMemoryCacheInvalidation.nuspec) 5 | $content = $content -replace '\$version\$',$versionStr 6 | 7 | $content | Out-File $root\RedisMemoryCacheInvalidation.compiled.nuspec 8 | 9 | & nuget pack $root\RedisMemoryCacheInvalidation.compiled.nuspec -------------------------------------------------------------------------------- /scripts/run_tests.ps1: -------------------------------------------------------------------------------- 1 | .\packages\OpenCover.4.6.166\tools\OpenCover.Console.exe -register:user -target:.\packages\xunit.runner.console.2.0.0\tools\xunit.console.x86.exe -targetargs:"""tests\RedisMemoryCacheInvalidation.Tests\bin\Release\RedisMemoryCacheInvalidation.Tests.dll"" -noshadow -appveyor -notrait ""category=Integration""" -filter:"+[RedisMemoryCacheInvalidation]*" -output:opencoverCoverage.xml 2 | 3 | $coveralls = (Resolve-Path "./packages/coveralls.net.*/tools/csmacnz.coveralls.exe").ToString() 4 | 5 | & $coveralls --opencover -i opencoverCoverage.xml --repoToken $env:COVERALLS_REPO_TOKEN --commitId $env:APPVEYOR_REPO_COMMIT --commitBranch $env:APPVEYOR_REPO_BRANCH --commitAuthor $env:APPVEYOR_REPO_COMMIT_AUTHOR --commitEmail $env:APPVEYOR_REPO_COMMIT_AUTHOR_EMAIL --commitMessage $env:APPVEYOR_REPO_COMMIT_MESSAGE --jobId $env:APPVEYOR_JOB_ID -------------------------------------------------------------------------------- /scripts/run_tests_local.ps1: -------------------------------------------------------------------------------- 1 | Remove-Item ../build/coverage -rec -force 2 | 3 | ..\packages\OpenCover.4.6.166\tools\OpenCover.Console.exe -register:user -target:..\packages\xunit.runner.console.2.0.0\tools\xunit.console.x86.exe -targetargs:"""..\tests\RedisMemoryCacheInvalidation.Tests\bin\Release\RedisMemoryCacheInvalidation.Tests.dll"" -noshadow -appveyor -notrait ""category=Integration""" -filter:"+[RedisMemoryCacheInvalidation]*" -output:..\build\opencoverCoverage.xml 4 | 5 | ..\packages\ReportGenerator.2.2.0.0\tools\ReportGenerator.exe -reports:..\build\opencoverCoverage.xml -targetdir:../build/coverage -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Constants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace RedisMemoryCacheInvalidation 8 | { 9 | public static class Constants 10 | { 11 | public const string DEFAULT_INVALIDATION_CHANNEL = "invalidate"; 12 | public const string DEFAULT_KEYSPACE_CHANNEL = "__keyevent*__:*"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Core/Interfaces/INotificationManager.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Core.Interfaces; 2 | using System; 3 | 4 | namespace RedisMemoryCacheInvalidation.Core 5 | { 6 | /// 7 | /// Manage a list of subscription. Basically a custom IObservable to support topic-based subscriptions. 8 | /// 9 | /// 10 | public interface INotificationManager 11 | { 12 | IDisposable Subscribe(string topic, INotificationObserver observer); 13 | 14 | void Notify(string topicKey); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Core/Interfaces/INotificationObserver.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace RedisMemoryCacheInvalidation.Core.Interfaces 3 | { 4 | /// 5 | /// Provides a mechanism for receiving push-based notifications. 6 | /// 7 | /// 8 | public interface INotificationObserver 9 | { 10 | void Notify(T value); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Core/Interfaces/IRedisNotificationBus.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Core; 2 | using RedisMemoryCacheInvalidation.Redis; 3 | using System.Runtime.Caching; 4 | using System.Threading.Tasks; 5 | 6 | namespace RedisMemoryCacheInvalidation 7 | { 8 | public interface IRedisNotificationBus 9 | { 10 | IRedisConnection Connection { get; } 11 | InvalidationStrategyType InvalidationStrategy { get; } 12 | MemoryCache LocalCache { get; } 13 | INotificationManager Notifier { get; } 14 | Task NotifyAsync(string key); 15 | void Start(); 16 | void Stop(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Core/NotificationManager.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Core.Interfaces; 2 | using RedisMemoryCacheInvalidation.Utils; 3 | using System; 4 | using System.Linq; 5 | using System.Collections.Concurrent; 6 | 7 | namespace RedisMemoryCacheInvalidation.Core 8 | { 9 | /// 10 | /// Manager subscriptions and notifications. 11 | /// 12 | internal class NotificationManager : INotificationManager 13 | { 14 | internal ConcurrentDictionary>> SubscriptionsByTopic { get; set; } 15 | 16 | public NotificationManager() 17 | { 18 | this.SubscriptionsByTopic = new ConcurrentDictionary>>(); 19 | } 20 | public void Notify(string topicKey) 21 | { 22 | var subscriptions = SubscriptionsByTopic.GetOrAdd(topicKey, new SynchronizedCollection>()); 23 | 24 | if (subscriptions.Count > 0) 25 | foreach (INotificationObserver observer in subscriptions.ToList()) //avoid collection modified 26 | { 27 | observer.Notify(topicKey); 28 | } 29 | } 30 | 31 | public IDisposable Subscribe(string topicKey, INotificationObserver observer) 32 | { 33 | var subscriptions = SubscriptionsByTopic.GetOrAdd(topicKey, new SynchronizedCollection>()); 34 | 35 | if (!subscriptions.Contains(observer)) 36 | subscriptions.Add(observer); 37 | 38 | return new Unsubscriber(subscriptions, observer); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Core/RedisNotificationBus.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Redis; 2 | using StackExchange.Redis; 3 | using System; 4 | using System.Runtime.Caching; 5 | using System.Threading.Tasks; 6 | 7 | namespace RedisMemoryCacheInvalidation.Core 8 | { 9 | /// 10 | /// Invalidation message bus. 11 | /// 12 | internal class RedisNotificationBus : IRedisNotificationBus 13 | { 14 | private readonly InvalidationSettings settings; 15 | public INotificationManager Notifier { get; private set; } 16 | public IRedisConnection Connection { get; internal set; } 17 | 18 | #region Props 19 | public InvalidationStrategyType InvalidationStrategy { get { return settings.InvalidationStrategy; } } 20 | public bool EnableKeySpaceNotifications { get { return settings.EnableKeySpaceNotifications; } } 21 | public MemoryCache LocalCache { get { return settings.TargetCache; } } 22 | public Action NotificationCallback { get { return settings.InvalidationCallback; } } 23 | #endregion 24 | 25 | #region Constructors 26 | private RedisNotificationBus(InvalidationSettings settings) 27 | { 28 | this.settings = settings; 29 | 30 | this.Notifier = new NotificationManager(); 31 | } 32 | 33 | public RedisNotificationBus(string redisConfiguration, InvalidationSettings settings) 34 | : this(settings) 35 | { 36 | this.Connection = RedisConnectionFactory.New(redisConfiguration); 37 | } 38 | 39 | public RedisNotificationBus(ConnectionMultiplexer mux, InvalidationSettings settings) 40 | : this(settings) 41 | { 42 | this.Connection = RedisConnectionFactory.New(mux); 43 | } 44 | #endregion 45 | 46 | public void Start() 47 | { 48 | this.Connection.Connect(); 49 | Connection.Subscribe(Constants.DEFAULT_INVALIDATION_CHANNEL, OnInvalidationMessage); 50 | if (this.EnableKeySpaceNotifications) 51 | Connection.Subscribe(Constants.DEFAULT_KEYSPACE_CHANNEL, OnKeySpaceNotificationMessage); 52 | } 53 | 54 | public void Stop() 55 | { 56 | this.Connection.Disconnect(); 57 | } 58 | 59 | public Task NotifyAsync(string key) 60 | { 61 | return this.Connection.PublishAsync(Constants.DEFAULT_INVALIDATION_CHANNEL, key); 62 | } 63 | 64 | #region Notification Handlers 65 | private void OnInvalidationMessage(RedisChannel pattern, RedisValue data) 66 | { 67 | if (pattern == Constants.DEFAULT_INVALIDATION_CHANNEL) 68 | { 69 | this.ProcessInvalidationMessage(data.ToString()); 70 | } 71 | } 72 | 73 | private void OnKeySpaceNotificationMessage(RedisChannel pattern, RedisValue data) 74 | { 75 | var prefix = pattern.ToString().Substring(0, 10); 76 | switch (prefix) 77 | { 78 | case "__keyevent": 79 | this.ProcessInvalidationMessage(data.ToString()); 80 | break; 81 | default: 82 | //nop 83 | break; 84 | } 85 | } 86 | #endregion 87 | 88 | private void ProcessInvalidationMessage(string key) 89 | { 90 | if ((this.InvalidationStrategy & InvalidationStrategyType.ChangeMonitor) == InvalidationStrategyType.ChangeMonitor) 91 | Notifier.Notify(key); 92 | 93 | if ((this.InvalidationStrategy & InvalidationStrategyType.AutoCacheRemoval) == InvalidationStrategyType.AutoCacheRemoval) 94 | if (this.LocalCache != null) 95 | this.LocalCache.Remove(key); 96 | 97 | if ((this.InvalidationStrategy & InvalidationStrategyType.External) == InvalidationStrategyType.External) 98 | if (NotificationCallback != null) 99 | NotificationCallback?.Invoke(key); 100 | 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Core/Unsubscriber.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Core.Interfaces; 2 | using RedisMemoryCacheInvalidation.Utils; 3 | using System; 4 | 5 | namespace RedisMemoryCacheInvalidation.Core 6 | { 7 | internal class Unsubscriber : IDisposable 8 | { 9 | private SynchronizedCollection> observers; 10 | private INotificationObserver observer; 11 | 12 | public Unsubscriber(SynchronizedCollection> observers, INotificationObserver observer) 13 | { 14 | Guard.NotNull(observers, nameof(observers)); 15 | Guard.NotNull(observer, nameof(observer)); 16 | 17 | this.observers = observers; 18 | this.observer = observer; 19 | } 20 | 21 | public void Dispose() 22 | { 23 | if (observer != null && observers.Contains(observer)) 24 | observers.Remove(observer); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/InvalidationManager.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Core; 2 | using RedisMemoryCacheInvalidation.Monitor; 3 | using RedisMemoryCacheInvalidation.Utils; 4 | using StackExchange.Redis; 5 | using System; 6 | using System.Runtime.Caching; 7 | using System.Threading.Tasks; 8 | 9 | namespace RedisMemoryCacheInvalidation 10 | { 11 | /// 12 | /// Libray's entry point. 13 | /// 14 | public static class InvalidationManager 15 | { 16 | internal static IRedisNotificationBus notificationBus; 17 | 18 | /// 19 | /// Redis connection state : connected or not. 20 | /// 21 | public static bool IsConnected 22 | { 23 | get {return notificationBus!=null && notificationBus.Connection.IsConnected;} 24 | } 25 | 26 | #region Setup 27 | /// 28 | /// Use to Configure Redis MemoryCache Invalidation. 29 | /// A new redis connection will be establish based upon parameter redisConfig. 30 | /// 31 | /// StackExchange configuration settings. 32 | /// InvalidationManager settings.( 33 | /// Task when connection is opened and subcribed to pubsub events. 34 | public static void Configure(string redisConfig, InvalidationSettings settings) 35 | { 36 | if (notificationBus == null) 37 | { 38 | notificationBus = new RedisNotificationBus(redisConfig, settings); 39 | notificationBus.Start(); 40 | } 41 | } 42 | 43 | /// 44 | /// Use to Configure Redis MemoryCache Invalidation. 45 | /// 46 | /// Reusing an existing ConnectionMultiplexer. 47 | /// InvalidationManager settings.( 48 | /// Task when connection is opened and subcribed to pubsub events. 49 | public static void Configure(ConnectionMultiplexer mux, InvalidationSettings settings) 50 | { 51 | if (notificationBus == null) 52 | { 53 | notificationBus = new RedisNotificationBus(mux, settings); 54 | notificationBus.Start(); 55 | } 56 | } 57 | #endregion 58 | 59 | #region CreateMonitor 60 | /// 61 | /// Allow to create a custom ChangeMonitor depending on the pubsub event (channel : invalidate, data:invalidationKey) 62 | /// 63 | /// invalidation key send by redis PUBLISH invalidate invalidatekey 64 | /// RedisChangeMonitor watching for notifications 65 | public static RedisChangeMonitor CreateChangeMonitor(string invalidationKey) 66 | { 67 | Guard.NotNullOrEmpty(invalidationKey, nameof(invalidationKey)); 68 | 69 | EnsureConfiguration(); 70 | 71 | if (notificationBus.InvalidationStrategy == InvalidationStrategyType.AutoCacheRemoval) 72 | throw new InvalidOperationException("Could not create a change monitor when InvalidationStrategy is DefaultMemoryCacheRemoval"); 73 | 74 | return new RedisChangeMonitor(notificationBus.Notifier, invalidationKey); 75 | } 76 | 77 | /// 78 | /// Allow to create a custom ChangeMonitor depending on the pubsub event (channel : invalidate, data:item.Key) 79 | /// 80 | /// todo: describe item parameter on CreateChangeMonitor 81 | /// RedisChangeMonitor watching for notifications 82 | public static RedisChangeMonitor CreateChangeMonitor(CacheItem item) 83 | { 84 | Guard.NotNull(item, nameof(item)); 85 | 86 | EnsureConfiguration(); 87 | 88 | return new RedisChangeMonitor(notificationBus.Notifier, item.Key); 89 | } 90 | #endregion 91 | 92 | /// 93 | /// Used to send invalidation message for a key. 94 | /// Shortcut for PUBLISH invalidate key. 95 | /// 96 | /// 97 | /// Task with the number of subscribers 98 | public static Task InvalidateAsync(string key) 99 | { 100 | Guard.NotNullOrEmpty(key, nameof(key)); 101 | 102 | EnsureConfiguration(); 103 | 104 | return notificationBus.NotifyAsync(key); 105 | } 106 | 107 | private static void EnsureConfiguration() 108 | { 109 | if (notificationBus == null) 110 | throw new InvalidOperationException("Configure() was not called"); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/InvalidationSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Caching; 3 | 4 | namespace RedisMemoryCacheInvalidation 5 | { 6 | public class InvalidationSettings 7 | { 8 | /// 9 | /// How to process invalidation message (remove from cache, invoke callback, notify change monitor, ...) 10 | /// 11 | public InvalidationStrategyType InvalidationStrategy { get; set; } 12 | /// 13 | /// Target MemoryCache when InvalidationStrategy=AutoCacheRemoval 14 | /// 15 | public MemoryCache TargetCache { get; set; } 16 | /// 17 | /// Subscribe to keyspace notification (if enabled on the redis DB) 18 | /// 19 | public bool EnableKeySpaceNotifications { get; set; } 20 | /// 21 | /// Invalidation callback invoked when InvalidationStrategy=External. 22 | /// 23 | public Action InvalidationCallback { get; set; } 24 | 25 | public InvalidationSettings() 26 | { 27 | this.InvalidationStrategy = InvalidationStrategyType.All; 28 | this.TargetCache = MemoryCache.Default; 29 | this.EnableKeySpaceNotifications = false; 30 | this.InvalidationCallback = null; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/InvalidationStrategyType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | namespace RedisMemoryCacheInvalidation 3 | { 4 | /// 5 | /// Cache Invalidation Strategy. 6 | /// 7 | [Flags] 8 | public enum InvalidationStrategyType 9 | { 10 | Undefined = 0, 11 | /// 12 | /// Use only change monitor to invalidate local items. 13 | /// 14 | ChangeMonitor=1, 15 | /// 16 | /// Auto remove items from default memory cache. 17 | /// 18 | AutoCacheRemoval=2, 19 | /// 20 | /// External. An event is emitted. 21 | /// 22 | External = 4, 23 | /// 24 | /// All. 25 | /// 26 | All = ~0 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Monitor/RedisChangeMonitor.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Core; 2 | using RedisMemoryCacheInvalidation.Core.Interfaces; 3 | using RedisMemoryCacheInvalidation.Utils; 4 | using System; 5 | using System.Globalization; 6 | using System.Runtime.Caching; 7 | 8 | namespace RedisMemoryCacheInvalidation.Monitor 9 | { 10 | public class RedisChangeMonitor : ChangeMonitor, INotificationObserver 11 | { 12 | private readonly string uniqueId; 13 | private readonly string key; 14 | private readonly IDisposable unsubscriber; 15 | /// 16 | /// Contructor. 17 | /// 18 | /// Registration handler 19 | /// invalidation Key 20 | public RedisChangeMonitor(INotificationManager notifier, string key) 21 | { 22 | Guard.NotNull(notifier, nameof(notifier)); 23 | Guard.NotNullOrEmpty(key, nameof(key)); 24 | 25 | var flag = true; 26 | try 27 | { 28 | this.unsubscriber = notifier.Subscribe(key, this); 29 | this.uniqueId = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); 30 | this.key = key; 31 | flag = false; 32 | } 33 | catch (Exception) 34 | { 35 | //any error 36 | flag = true; 37 | } 38 | finally 39 | { 40 | base.InitializationComplete(); 41 | if (flag) 42 | { 43 | base.Dispose(); 44 | } 45 | } 46 | } 47 | 48 | protected override void Dispose(bool disposing) 49 | { 50 | // always Unsubscribe on dispose 51 | this.Unsubscribe(); 52 | } 53 | 54 | public override string UniqueId 55 | { 56 | get { return this.uniqueId; } 57 | } 58 | 59 | #region INotification 60 | public void Notify(string value) 61 | { 62 | if (value == key) 63 | base.OnChanged(null); 64 | } 65 | #endregion 66 | 67 | private void Unsubscribe() 68 | { 69 | if (this.unsubscriber != null) 70 | this.unsubscriber.Dispose(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("RedisMemoryCacheInvalidation")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Cybermaxs")] 12 | [assembly: AssemblyProduct("RedisMemoryCacheInvalidation")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("f5144c1c-3053-4d47-baf0-9af95792ab95")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.14474.1")] 36 | [assembly: AssemblyFileVersion("1.0.14474.1")] 37 | 38 | [assembly: InternalsVisibleTo("RedisMemoryCacheInvalidation.Tests")] 39 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Redis/ExistingRedisConnnection.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Utils; 2 | using StackExchange.Redis; 3 | 4 | namespace RedisMemoryCacheInvalidation.Redis 5 | { 6 | internal class ExistingRedisConnnection : RedisConnectionBase 7 | { 8 | public ExistingRedisConnnection(IConnectionMultiplexer mux) 9 | { 10 | multiplexer = mux; 11 | } 12 | 13 | public override bool Connect() 14 | { 15 | return this.IsConnected; 16 | } 17 | 18 | public override void Disconnect() 19 | { 20 | this.UnsubscribeAll(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Redis/IRedisConnection.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace RedisMemoryCacheInvalidation.Redis 7 | { 8 | public interface IRedisConnection 9 | { 10 | bool IsConnected { get; } 11 | bool Connect(); 12 | void Disconnect(); 13 | Task[]> GetConfigAsync(); 14 | void Subscribe(string channel, Action handler); 15 | void UnsubscribeAll(); 16 | Task PublishAsync(string channel, string value); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Redis/RedisConnectionBase.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Utils; 2 | using StackExchange.Redis; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading.Tasks; 6 | 7 | namespace RedisMemoryCacheInvalidation.Redis 8 | { 9 | internal abstract class RedisConnectionBase : IRedisConnection 10 | { 11 | protected IConnectionMultiplexer multiplexer; 12 | 13 | #region IRedisConnection 14 | public bool IsConnected 15 | { 16 | get { return this.multiplexer != null && this.multiplexer.IsConnected; } 17 | } 18 | 19 | public void Subscribe(string channel, Action handler) 20 | { 21 | if (this.IsConnected) 22 | { 23 | var subscriber = this.multiplexer.GetSubscriber(); 24 | subscriber.Subscribe(channel, handler); 25 | } 26 | } 27 | 28 | public void UnsubscribeAll() 29 | { 30 | if (this.IsConnected) 31 | this.multiplexer.GetSubscriber().UnsubscribeAll(); 32 | } 33 | 34 | public Task PublishAsync(string channel, string value) 35 | { 36 | if (this.IsConnected) 37 | { 38 | return this.multiplexer.GetSubscriber().PublishAsync(channel, value); 39 | } 40 | else 41 | return TaskCache.FromResult(0L); 42 | } 43 | public Task[]> GetConfigAsync() 44 | { 45 | if (this.IsConnected) 46 | { 47 | var server = this.GetServer(); 48 | return server.ConfigGetAsync(); 49 | } 50 | else 51 | return TaskCache.FromResult(new KeyValuePair[] { }); 52 | } 53 | #endregion 54 | 55 | #region privates 56 | protected IServer GetServer() 57 | { 58 | var endpoints = this.multiplexer.GetEndPoints(); 59 | IServer result = null; 60 | foreach (var endpoint in endpoints) 61 | { 62 | var server = this.multiplexer.GetServer(endpoint); 63 | if (server.IsSlave || !server.IsConnected) continue; 64 | if (result != null) throw new InvalidOperationException("Requires exactly one master endpoint (found " + server.EndPoint + " and " + result.EndPoint + ")"); 65 | result = server; 66 | } 67 | if (result == null) throw new InvalidOperationException("Requires exactly one master endpoint (found none)"); 68 | return result; 69 | } 70 | 71 | public abstract bool Connect(); 72 | 73 | public abstract void Disconnect(); 74 | #endregion 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Redis/RedisConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | 3 | namespace RedisMemoryCacheInvalidation.Redis 4 | { 5 | internal class RedisConnectionFactory 6 | { 7 | public static IRedisConnection New(IConnectionMultiplexer mux) 8 | { 9 | return new ExistingRedisConnnection(mux); 10 | } 11 | 12 | public static IRedisConnection New(string options) 13 | { 14 | return new StandaloneRedisConnection(options); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Redis/StandaloneRedisConnection.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | using System; 3 | using System.Reflection; 4 | 5 | namespace RedisMemoryCacheInvalidation.Redis 6 | { 7 | internal class StandaloneRedisConnection : RedisConnectionBase 8 | { 9 | private readonly ConfigurationOptions options; 10 | public StandaloneRedisConnection(string configurationOptions) 11 | { 12 | this.options = ConfigurationOptions.Parse(configurationOptions); 13 | } 14 | 15 | public override bool Connect() 16 | { 17 | if (this.multiplexer == null) 18 | { 19 | //myope overrides here 20 | this.options.ConnectTimeout = 5000; 21 | this.options.ConnectRetry = 3; 22 | this.options.DefaultVersion = new Version("2.8.0"); 23 | this.options.KeepAlive = 90; 24 | this.options.AbortOnConnectFail = false; 25 | this.options.ClientName = "InvalidationClient_" + System.Environment.MachineName + "_" + Assembly.GetCallingAssembly().GetName().Version; 26 | 27 | this.multiplexer = ConnectionMultiplexer.Connect(options); 28 | } 29 | 30 | return this.multiplexer.IsConnected; 31 | } 32 | 33 | public override void Disconnect() 34 | { 35 | this.UnsubscribeAll(); 36 | this.multiplexer.Close(false); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/RedisMemoryCacheInvalidation.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {13467724-727B-44CA-BE9D-19BFC4599B34} 8 | Library 9 | Properties 10 | RedisMemoryCacheInvalidation 11 | RedisMemoryCacheInvalidation 12 | v4.5 13 | 512 14 | ..\..\ 15 | true 16 | 17 | 6483b00a 18 | 19 | 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | false 28 | 29 | 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | false 37 | 38 | 39 | 40 | ..\..\packages\StackExchange.Redis.1.0.481\lib\net45\StackExchange.Redis.dll 41 | True 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 81 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Utils/Guard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace RedisMemoryCacheInvalidation.Utils 5 | { 6 | internal static class Guard 7 | { 8 | /// 9 | /// Ensures the value of the given is not null. 10 | /// Throws otherwise. 11 | /// 12 | /// instance to test for null 13 | /// nameof the parameter (for ArgumentNullException) 14 | public static void NotNull(object parameter, string parameterName) 15 | { 16 | if (parameter == null) 17 | throw new ArgumentNullException(parameterName); 18 | } 19 | 20 | /// 21 | /// Ensures the value of the given is not null. 22 | /// Throws otherwise. 23 | /// 24 | /// instance to test for (Generic) Default value 25 | /// nameof the parameter (for ArgumentNullException) 26 | public static void NotDefault(T parameter, string parameterName) 27 | { 28 | if (EqualityComparer.Default.Equals(parameter, default(T))) 29 | throw new ArgumentNullException(parameterName); 30 | } 31 | 32 | /// 33 | /// Ensures the string value of the given is not null or empty. 34 | /// Throws in the first case, or 35 | /// in the latter. 36 | /// 37 | /// string to test for Null or empty 38 | /// nameof the parameter (for ArgumentNullException) 39 | public static void NotNullOrEmpty(string parameter, string parameterName) 40 | { 41 | if (string.IsNullOrEmpty(parameter)) 42 | throw new ArgumentNullException(parameterName); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Utils/SynchronizedCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | using System.Text; 7 | 8 | namespace RedisMemoryCacheInvalidation.Utils 9 | { 10 | /// 11 | /// Inspired by System.ServiceModel.SynchronizedCollection 12 | /// https://github.com/Microsoft/referencesource/blob/master/System.ServiceModel/System/ServiceModel/SynchronizedCollection.cs 13 | /// 14 | /// 15 | [ExcludeFromCodeCoverage] 16 | public class SynchronizedCollection : ICollection 17 | { 18 | private List items; 19 | private object sync; 20 | 21 | public SynchronizedCollection() 22 | { 23 | this.items = new List(); 24 | this.sync = new object(); 25 | } 26 | 27 | public int Count 28 | { 29 | get { lock (this.sync) { return this.items.Count; } } 30 | } 31 | 32 | public void Add(T item) 33 | { 34 | lock (this.sync) 35 | { 36 | int index = this.items.Count; 37 | this.items.Insert(index, item); 38 | } 39 | } 40 | 41 | public void Clear() 42 | { 43 | lock (this.sync) 44 | { 45 | this.items.Clear(); 46 | } 47 | } 48 | 49 | public void CopyTo(T[] array, int index) 50 | { 51 | lock (this.sync) 52 | { 53 | this.items.CopyTo(array, index); 54 | } 55 | } 56 | 57 | public bool Contains(T item) 58 | { 59 | lock (this.sync) 60 | { 61 | return this.items.Contains(item); 62 | } 63 | } 64 | 65 | public IEnumerator GetEnumerator() 66 | { 67 | lock (this.sync) 68 | { 69 | return this.items.GetEnumerator(); 70 | } 71 | } 72 | 73 | int InternalIndexOf(T item) 74 | { 75 | int count = items.Count; 76 | 77 | for (int i = 0; i < count; i++) 78 | { 79 | if (object.Equals(items[i], item)) 80 | { 81 | return i; 82 | } 83 | } 84 | return -1; 85 | } 86 | 87 | public bool Remove(T item) 88 | { 89 | lock (this.sync) 90 | { 91 | int index = this.InternalIndexOf(item); 92 | if (index < 0) 93 | return false; 94 | 95 | this.items.RemoveAt(index); 96 | return true; 97 | } 98 | } 99 | 100 | bool ICollection.IsReadOnly 101 | { 102 | get { return false; } 103 | } 104 | 105 | IEnumerator IEnumerable.GetEnumerator() 106 | { 107 | return ((IList)this.items).GetEnumerator(); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/Utils/TaskCache.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace RedisMemoryCacheInvalidation.Utils 4 | { 5 | internal class TaskCache 6 | { 7 | public static readonly Task AlwaysTrue = MakeTask(true); 8 | public static readonly Task AlwaysFalse = MakeTask(false); 9 | public static readonly Task Empty = MakeTask(null); 10 | 11 | private static Task MakeTask(T value) 12 | { 13 | return FromResult(value); 14 | } 15 | 16 | public static Task FromResult(T value) 17 | { 18 | var tcs = new TaskCompletionSource(); 19 | tcs.SetResult(value); 20 | return tcs.Task; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation_Net40/RedisMemoryCacheInvalidation_Net40.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {CC9DCBE7-B6B8-4F13-9768-B90DB619B8DB} 8 | Library 9 | Properties 10 | RedisMemoryCacheInvalidation 11 | RedisMemoryCacheInvalidation 12 | v4.0 13 | 512 14 | ..\..\ 15 | true 16 | 17 | 18 | 19 | 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.dll 39 | True 40 | 41 | 42 | ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll 43 | True 44 | 45 | 46 | ..\..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll 47 | True 48 | 49 | 50 | ..\..\packages\StackExchange.Redis.1.0.481\lib\net40\StackExchange.Redis.dll 51 | True 52 | 53 | 54 | 55 | 56 | ..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.IO.dll 57 | True 58 | 59 | 60 | 61 | 62 | ..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.Runtime.dll 63 | True 64 | 65 | 66 | 67 | ..\..\packages\Microsoft.Bcl.1.1.10\lib\net40\System.Threading.Tasks.dll 68 | True 69 | 70 | 71 | 72 | 73 | %(RecursiveDir)%(FileName) 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 88 | -------------------------------------------------------------------------------- /src/RedisMemoryCacheInvalidation_Net40/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Core/InvalidationManagerTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using System; 3 | using System.Runtime.Caching; 4 | using Xunit; 5 | 6 | namespace RedisMemoryCacheInvalidation.Tests 7 | { 8 | public class InvalidationManagerTests 9 | { 10 | private readonly Mock mockOfBus; 11 | 12 | 13 | public InvalidationManagerTests() 14 | { 15 | InvalidationManager.notificationBus = null; 16 | mockOfBus = new Mock(); 17 | } 18 | 19 | #region Configure 20 | [Fact] 21 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 22 | public void Configure_WhenInvalid_ShouldThrowException() 23 | { 24 | InvalidationManager.Configure("dfsdf", new InvalidationSettings()); 25 | 26 | Assert.False(InvalidationManager.IsConnected); 27 | } 28 | 29 | [Fact] 30 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 31 | public void Configure_WhenTwice_ShouldNotThrowException() 32 | { 33 | InvalidationManager.Configure("dfsdf", new InvalidationSettings()); 34 | InvalidationManager.Configure("dfsdf", new InvalidationSettings()); 35 | } 36 | #endregion 37 | 38 | #region Invalid Parameters 39 | 40 | [Fact] 41 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 42 | public void CreateChangeMonitorBadArgs_ShouldThrowException() 43 | { 44 | Assert.Throws(() => { InvalidationManager.CreateChangeMonitor("rzer"); }); 45 | Assert.Throws(() => { InvalidationManager.CreateChangeMonitor(new CacheItem("rzesdqr")); }); 46 | Assert.Throws(() => { InvalidationManager.InvalidateAsync("rzaaer"); }); 47 | Assert.Throws(() => { InvalidationManager.CreateChangeMonitor((string)null); }); 48 | Assert.Throws(() => { InvalidationManager.CreateChangeMonitor((CacheItem)null); }); 49 | } 50 | #endregion 51 | 52 | [Fact] 53 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 54 | public void Invalidate_WhenInvalid_ShouldPublushToRedis() 55 | { 56 | InvalidationManager.notificationBus = this.mockOfBus.Object; 57 | InvalidationManager.InvalidateAsync("mykey"); 58 | this.mockOfBus.Verify(b => b.NotifyAsync("mykey"), Times.Once); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Core/NotificationManagerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using RedisMemoryCacheInvalidation.Core; 3 | using RedisMemoryCacheInvalidation.Core.Interfaces; 4 | using Moq; 5 | using Xunit; 6 | using Ploeh.AutoFixture; 7 | 8 | namespace RedisMemoryCacheInvalidation.Tests.Core 9 | { 10 | public class NotificationManagerTests 11 | { 12 | private Fixture fixture = new Fixture(); 13 | private readonly string topciKey; 14 | public NotificationManagerTests() 15 | { 16 | topciKey = fixture.Create(); 17 | } 18 | [Fact] 19 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 20 | public void WhenSubscribed_ShouldPass() 21 | { 22 | var mockOfObserver = new Mock>(); 23 | var notifier = new NotificationManager(); 24 | var res = notifier.Subscribe(topciKey, mockOfObserver.Object); 25 | 26 | Assert.NotNull(res); 27 | Assert.Equal(1, notifier.SubscriptionsByTopic.Values.Count); 28 | Assert.True(notifier.SubscriptionsByTopic.Values.SelectMany(e=>e).Contains(mockOfObserver.Object)); 29 | Assert.IsType(res); 30 | } 31 | 32 | [Fact] 33 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 34 | public void WhenSubscribedTwice_ShouldBeSubscriberOnlyOnce() 35 | { 36 | var mockOfObserver = new Mock>(); 37 | var notifier = new NotificationManager(); 38 | var res1 = notifier.Subscribe(topciKey, mockOfObserver.Object); 39 | var res2 = notifier.Subscribe(topciKey, mockOfObserver.Object); 40 | 41 | Assert.NotNull(res1); 42 | Assert.NotSame(res1, res2); 43 | Assert.Equal(1, notifier.SubscriptionsByTopic.Values.Count); 44 | Assert.True(notifier.SubscriptionsByTopic.Values.SelectMany(e => e).Contains(mockOfObserver.Object)); 45 | Assert.IsType(res1); 46 | } 47 | 48 | [Fact] 49 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 50 | public void WhenSameTopic_ShouldNotifyAll() 51 | { 52 | var mockOfObserver1 = new Mock>(); 53 | var mockOfObserver2 = new Mock>(); 54 | var notifier = new NotificationManager(); 55 | var res1 = notifier.Subscribe(topciKey, mockOfObserver1.Object); 56 | var res2 = notifier.Subscribe(topciKey, mockOfObserver2.Object); 57 | 58 | notifier.Notify(topciKey); 59 | 60 | Assert.NotNull(notifier.SubscriptionsByTopic.Values.SelectMany(e => e).Count() == 0); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Core/RedisNotificationBusTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using RedisMemoryCacheInvalidation.Core; 3 | using RedisMemoryCacheInvalidation.Monitor; 4 | using RedisMemoryCacheInvalidation.Redis; 5 | using StackExchange.Redis; 6 | using System; 7 | using System.Runtime.Caching; 8 | using Xunit; 9 | 10 | namespace RedisMemoryCacheInvalidation.Tests.Core 11 | { 12 | public class RedisNotificationBusTests 13 | { 14 | Mock MockOfConnection { get; set; } 15 | Action NotificationEmitter { get; set; } 16 | private void Invalidate(string topic, Action handler) 17 | { 18 | this.NotificationEmitter = handler; 19 | } 20 | 21 | public RedisNotificationBusTests() 22 | { 23 | this.MockOfConnection = new Mock(); 24 | this.MockOfConnection.Setup(c => c.PublishAsync(It.IsAny(), It.IsAny())).ReturnsAsync(5); 25 | this.MockOfConnection.Setup(c => c.Subscribe(It.IsAny(), It.IsAny>())).Callback>(this.Invalidate); 26 | this.MockOfConnection.Setup(c => c.Connect()).Returns(true); 27 | this.MockOfConnection.Setup(c => c.Disconnect()); 28 | } 29 | 30 | [Fact] 31 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 32 | public void RedisNotificationBus_WhenInvalidCtorArgs_ShouldNotThrowExceptions() 33 | { 34 | var bus = new RedisNotificationBus("fghfgh", new InvalidationSettings()); 35 | Assert.NotNull(bus.Connection); 36 | Assert.NotNull(bus.Notifier); 37 | Assert.Equal(MemoryCache.Default, bus.LocalCache); 38 | } 39 | 40 | [Fact] 41 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 42 | public void RedisNotificationBus_WhenStart_ShouldConnectAndSubscribe() 43 | { 44 | var bus = new RedisNotificationBus("localhost:6379", new InvalidationSettings()); 45 | bus.Connection = this.MockOfConnection.Object; 46 | 47 | bus.Start(); 48 | 49 | this.MockOfConnection.Verify(c => c.Connect(), Times.Once); 50 | this.MockOfConnection.Verify(c => c.Subscribe(Constants.DEFAULT_INVALIDATION_CHANNEL, It.IsAny>()), Times.Once); 51 | } 52 | 53 | [Fact] 54 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 55 | public void RedisNotificationBus_WhenNotify_ShouldPublishAsync() 56 | { 57 | var bus = new RedisNotificationBus("localhost:6379", new InvalidationSettings()); 58 | bus.Connection = this.MockOfConnection.Object; 59 | 60 | 61 | var notifyTask = bus.NotifyAsync("mykey"); 62 | 63 | Assert.NotNull(notifyTask); 64 | Assert.Equal(5, notifyTask.Result); 65 | this.MockOfConnection.Verify(c => c.PublishAsync(Constants.DEFAULT_INVALIDATION_CHANNEL, "mykey"), Times.Once); 66 | } 67 | 68 | [Fact] 69 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 70 | public void RedisNotificationBus_WhenStop_ShouldDisconnect() 71 | { 72 | var bus = new RedisNotificationBus("localhost:6379", new InvalidationSettings()); 73 | bus.Connection = this.MockOfConnection.Object; 74 | 75 | 76 | var notifyTask = bus.NotifyAsync("mykey"); 77 | 78 | Assert.NotNull(notifyTask); 79 | Assert.Equal(5, notifyTask.Result); 80 | this.MockOfConnection.Verify(c => c.PublishAsync(Constants.DEFAULT_INVALIDATION_CHANNEL, "mykey"), Times.Once); 81 | } 82 | 83 | 84 | #region InvalidationMessage 85 | [Fact] 86 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 87 | public void RedisNotificationBus_WhenInvalidation_ShouldRemoveFromDefaultCache() 88 | { 89 | var lcache = new MemoryCache(Guid.NewGuid().ToString()); 90 | var bus = new RedisNotificationBus("localhost:6379", new InvalidationSettings() { TargetCache = lcache, InvalidationStrategy = InvalidationStrategyType.AutoCacheRemoval }); 91 | bus.Connection = this.MockOfConnection.Object; 92 | var monitor = new RedisChangeMonitor(bus.Notifier, "mykey"); 93 | lcache.Add("mykey", DateTime.UtcNow, new CacheItemPolicy() { AbsoluteExpiration = DateTime.UtcNow.AddDays(1), ChangeMonitors = { monitor } }); 94 | 95 | bus.Start(); 96 | 97 | //act 98 | this.NotificationEmitter(Constants.DEFAULT_INVALIDATION_CHANNEL, "mykey"); 99 | 100 | //assert 101 | Assert.False(lcache.Contains("mykey")); 102 | Assert.True(monitor.IsDisposed); 103 | } 104 | 105 | [Fact] 106 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 107 | public void RedisNotificationBus_WhenInvalidation_ShouldDisposeMonitor() 108 | { 109 | var lcache = new MemoryCache(Guid.NewGuid().ToString()); 110 | var bus = new RedisNotificationBus("localhost:6379", new InvalidationSettings() { TargetCache = lcache, InvalidationStrategy = InvalidationStrategyType.ChangeMonitor }); 111 | bus.Connection = this.MockOfConnection.Object; 112 | var monitor = new RedisChangeMonitor(bus.Notifier, "mykey"); 113 | lcache.Add("mykey", DateTime.UtcNow, new CacheItemPolicy() { AbsoluteExpiration = DateTime.UtcNow.AddDays(1), ChangeMonitors = { monitor } }); 114 | 115 | bus.Start(); 116 | 117 | //act 118 | this.NotificationEmitter(Constants.DEFAULT_INVALIDATION_CHANNEL, "mykey"); 119 | 120 | //assert 121 | Assert.False(lcache.Contains("mykey")); 122 | Assert.True(monitor.IsDisposed); 123 | } 124 | 125 | [Fact] 126 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 127 | public void RedisNotificationBus_WhenInvalidation_ShouldInvokeCallback() 128 | { 129 | var lcache = new MemoryCache(Guid.NewGuid().ToString()); 130 | var called=false; 131 | Action cb= s=> {called=true;}; 132 | var bus = new RedisNotificationBus("localhost:6379", new InvalidationSettings() { InvalidationStrategy = InvalidationStrategyType.External, InvalidationCallback = cb }); 133 | bus.Connection = this.MockOfConnection.Object; 134 | var monitor = new RedisChangeMonitor(bus.Notifier, "mykey"); 135 | lcache.Add("mykey", DateTime.UtcNow, new CacheItemPolicy() { AbsoluteExpiration = DateTime.UtcNow.AddDays(1), ChangeMonitors = { monitor } }); 136 | 137 | bus.Start(); 138 | 139 | //act 140 | this.NotificationEmitter(Constants.DEFAULT_INVALIDATION_CHANNEL, "mykey"); 141 | 142 | //assert 143 | Assert.True(lcache.Contains("mykey")); 144 | Assert.False(monitor.IsDisposed); 145 | Assert.True(called); 146 | } 147 | #endregion 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Core/UnsubscriberTests.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Core; 2 | using RedisMemoryCacheInvalidation.Core.Interfaces; 3 | using Moq; 4 | using RedisMemoryCacheInvalidation.Utils; 5 | using Xunit; 6 | using System; 7 | 8 | namespace RedisMemoryCacheInvalidation.Tests.Core 9 | { 10 | public class UnsubscriberTests 11 | { 12 | [Fact] 13 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 14 | public void WhenCtorBadArgs_ShouldThrowExceptions() 15 | { 16 | Assert.Throws(() => 17 | { 18 | var mock = new Mock>(); 19 | var s = new Unsubscriber(null, mock.Object); 20 | }); 21 | Assert.Throws(() => 22 | { 23 | var obs = new SynchronizedCollection> { }; 24 | var s = new Unsubscriber(obs, null); 25 | }); 26 | } 27 | 28 | [Fact] 29 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 30 | public void WhenUnsubscribe_ShouldBeDisposed() 31 | { 32 | var mock1 = new Mock>(); 33 | var mock2 = new Mock>(); 34 | var mock3 = new Mock>(); 35 | 36 | var obs = new SynchronizedCollection> {mock1.Object, mock2.Object, mock3.Object }; 37 | var unsub = new Unsubscriber(obs, mock2.Object); 38 | unsub.Dispose(); 39 | 40 | Assert.Equal(2, obs.Count); 41 | Assert.False(obs.Contains(mock2.Object)); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Fixture/RedisCollection.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace RedisMemoryCacheInvalidation.Tests.Fixtures 4 | { 5 | [CollectionDefinition("RedisServer")] 6 | public class RedisCollection : ICollectionFixture 7 | { 8 | // This class has no code, and is never created. Its purpose is simply 9 | // to be the place to apply [CollectionDefinition] and all the 10 | // ICollectionFixture<> interfaces. 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Fixture/RedisServerFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Diagnostics; 4 | using StackExchange.Redis; 5 | using System.Threading; 6 | 7 | namespace RedisMemoryCacheInvalidation.Tests.Fixtures 8 | { 9 | public class RedisServerFixture : IDisposable 10 | { 11 | private Process server; 12 | private ConnectionMultiplexer mux; 13 | 14 | private bool wasStarted; 15 | 16 | public RedisServerFixture() 17 | { 18 | if (!IsRunning) 19 | { 20 | this.server = Process.Start(@"..\..\..\..\packages\Redis-64.2.8.21\redis-server.exe"); 21 | wasStarted = true; 22 | } 23 | Thread.Sleep(1000); 24 | this.mux = ConnectionMultiplexer.Connect("localhost:6379,allowAdmin=true"); 25 | this.mux.GetServer("localhost: 6379").ConfigSet("notify-keyspace-events", "KEA"); 26 | } 27 | 28 | public void Dispose() 29 | { 30 | if (this.mux != null && this.mux.IsConnected) 31 | this.mux.Close(false); 32 | 33 | if (server != null && !server.HasExited && wasStarted) 34 | server.Kill(); 35 | } 36 | 37 | public IDatabase GetDatabase(int db) 38 | { 39 | return this.mux.GetDatabase(db); 40 | } 41 | 42 | public ISubscriber GetSubscriber() 43 | { 44 | return this.mux.GetSubscriber(); 45 | } 46 | 47 | public static bool IsRunning 48 | { 49 | get 50 | { 51 | return Process.GetProcessesByName("redis-server").Count() > 0; 52 | } 53 | } 54 | 55 | public void Reset() 56 | { 57 | this.mux.GetServer("localhost:6379").FlushAllDatabases(); 58 | } 59 | 60 | public static void Kill() 61 | { 62 | foreach (var p in Process.GetProcessesByName(@"redis-server")) 63 | { 64 | p.Kill(); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Integration/DisconnectedTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace RedisMemoryCacheInvalidation.Tests.Integration 4 | { 5 | public class DisconnectedTests 6 | { 7 | public DisconnectedTests() 8 | { 9 | InvalidationManager.notificationBus = null; 10 | } 11 | 12 | [Fact] 13 | [Trait(TestConstants.TestCategory, TestConstants.IntegrationTestCategory)] 14 | public void InvalidHost_ShouldNotBeConnected() 15 | { 16 | //test more disconnected scenarios 17 | InvalidationManager.Configure("blabblou", new InvalidationSettings()); 18 | Assert.False(InvalidationManager.IsConnected); 19 | 20 | var published = InvalidationManager.InvalidateAsync("mykey").Result; 21 | Assert.Equal(0L, published); 22 | } 23 | 24 | [Fact] 25 | [Trait(TestConstants.TestCategory, TestConstants.IntegrationTestCategory)] 26 | public void WhenNotConnected_ShouldNotPublishMessages() 27 | { 28 | //test more disconnected scenarios 29 | InvalidationManager.Configure("blabblou", new InvalidationSettings()); 30 | 31 | var published = InvalidationManager.InvalidateAsync("mykey").Result; 32 | Assert.Equal(0L, published); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Integration/InvalidationTests.cs: -------------------------------------------------------------------------------- 1 | using Ploeh.AutoFixture; 2 | using RedisMemoryCacheInvalidation.Monitor; 3 | using RedisMemoryCacheInvalidation.Tests; 4 | using RedisMemoryCacheInvalidation.Tests.Fixtures; 5 | using System; 6 | using System.Runtime.Caching; 7 | using System.Text; 8 | using System.Threading; 9 | using Xunit; 10 | 11 | namespace RedisMemoryCacheInvalidation.Integration.Tests 12 | { 13 | [Collection("RedisServer")] 14 | public class InvalidationTests 15 | { 16 | private readonly MemoryCache localCache; 17 | private readonly Fixture fixture; 18 | private RedisServerFixture redis; 19 | 20 | public InvalidationTests(RedisServerFixture redisServer) 21 | { 22 | localCache = new MemoryCache(Guid.NewGuid().ToString()); 23 | localCache.Trim(100); 24 | 25 | redisServer.Reset(); 26 | redis = redisServer; 27 | 28 | InvalidationManager.notificationBus = null; 29 | InvalidationManager.Configure("localhost:6379", new InvalidationSettings { 30 | InvalidationStrategy = InvalidationStrategyType.All, 31 | EnableKeySpaceNotifications = true, 32 | TargetCache = localCache }); 33 | 34 | fixture = new Fixture(); 35 | } 36 | 37 | [Fact] 38 | [Trait(TestConstants.TestCategory, TestConstants.IntegrationTestCategory)] 39 | public void MultiplesDeps_WhenChangeMonitor_WhenInvalidation_ShouldRemoved() 40 | { 41 | var baseCacheKey = fixture.Create(); 42 | var invalidationKey = fixture.Create(); 43 | 44 | var monitor1 = InvalidationManager.CreateChangeMonitor(invalidationKey); 45 | var monitor2 = InvalidationManager.CreateChangeMonitor(invalidationKey); 46 | 47 | CreateCacheItemAndAdd(localCache, baseCacheKey + "1", monitor1); 48 | CreateCacheItemAndAdd(localCache, baseCacheKey + "2", monitor2); 49 | 50 | Assert.Equal(2, localCache.GetCount()); 51 | Assert.False(monitor1.IsDisposed, "should not be removed before notification"); 52 | Assert.False(monitor2.IsDisposed, "should not be removed before notification"); 53 | 54 | //act 55 | var subscriber = redis.GetSubscriber(); 56 | subscriber.Publish(Constants.DEFAULT_INVALIDATION_CHANNEL, Encoding.Default.GetBytes(invalidationKey)); 57 | 58 | // hack wait for notif 59 | Thread.Sleep(50); 60 | 61 | //assert 62 | Assert.False(localCache.Contains(baseCacheKey + "1"), "cache item shoud be removed"); 63 | Assert.False(localCache.Contains(baseCacheKey + "2"), "cache item shoud be removed"); 64 | Assert.True(monitor1.IsDisposed, "should be disposed"); 65 | Assert.True(monitor2.IsDisposed, "should be disposed"); 66 | } 67 | 68 | [Fact] 69 | [Trait(TestConstants.TestCategory, TestConstants.IntegrationTestCategory)] 70 | public void MultiplesDeps_WhenImplicitRemoval_WhenInvalidation_ShouldRemoved() 71 | { 72 | var baseCacheKey = fixture.Create(); 73 | 74 | CreateCacheItemAndAdd(localCache, baseCacheKey + "1"); 75 | CreateCacheItemAndAdd(localCache, baseCacheKey + "2"); 76 | 77 | Assert.Equal(2, localCache.GetCount()); 78 | 79 | // act 80 | InvalidationManager.InvalidateAsync(baseCacheKey + "1").Wait(); 81 | InvalidationManager.InvalidateAsync(baseCacheKey + "2").Wait(); 82 | 83 | Thread.Sleep(50); 84 | 85 | //assert 86 | Assert.Equal(0, localCache.GetCount()); 87 | Assert.False(localCache.Contains(baseCacheKey + "1"), "cache item shoud be removed"); 88 | Assert.False(localCache.Contains(baseCacheKey + "2"), "cache item shoud be removed"); 89 | } 90 | 91 | 92 | [Fact] 93 | [Trait(TestConstants.TestCategory, TestConstants.IntegrationTestCategory)] 94 | public void MultiplesDeps_WhenSpaceNotification_ShouldBeRemoved() 95 | { 96 | var baseCacheKey = fixture.Create(); 97 | var invalidationKey = fixture.Create(); 98 | 99 | var monitor1 = InvalidationManager.CreateChangeMonitor(invalidationKey); 100 | var monitor2 = InvalidationManager.CreateChangeMonitor(invalidationKey); 101 | 102 | CreateCacheItemAndAdd(localCache, baseCacheKey + "1", monitor1); 103 | CreateCacheItemAndAdd(localCache, baseCacheKey + "2", monitor2); 104 | 105 | // act 106 | var db = redis.GetDatabase(0); 107 | db.StringSet(invalidationKey, "notused"); 108 | 109 | Thread.Sleep(200); 110 | 111 | //assert 112 | Assert.Equal(0, localCache.GetCount()); 113 | Assert.False(localCache.Contains(baseCacheKey + "1"), "cache item shoud be removed"); 114 | Assert.False(localCache.Contains(baseCacheKey + "2"), "cache item shoud be removed"); 115 | } 116 | 117 | private static CacheItem CreateCacheItemAndAdd(MemoryCache target, string cacheKey, RedisChangeMonitor monitor = null) 118 | { 119 | var cacheItem = new CacheItem(cacheKey, DateTime.Now); 120 | var policy = new CacheItemPolicy 121 | { 122 | AbsoluteExpiration = DateTime.UtcNow.AddDays(1) 123 | }; 124 | if (monitor != null) 125 | policy.ChangeMonitors.Add(monitor); 126 | target.Add(cacheItem, policy); 127 | return cacheItem; 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Monitor/RedisChangeMonitorTest.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Ploeh.AutoFixture; 3 | using RedisMemoryCacheInvalidation.Core; 4 | using RedisMemoryCacheInvalidation.Core.Interfaces; 5 | using RedisMemoryCacheInvalidation.Monitor; 6 | using System; 7 | using Xunit; 8 | 9 | namespace RedisMemoryCacheInvalidation.Tests.Monitor 10 | { 11 | public class RedisChangeMonitorTest 12 | { 13 | private readonly string notifKey; 14 | 15 | private readonly Fixture fixture = new Fixture(); 16 | private readonly Mock> mockOfBus; 17 | private readonly Mock mockOfDispose; 18 | 19 | INotificationManager Bus { get { return mockOfBus.Object; } } 20 | 21 | public RedisChangeMonitorTest() 22 | { 23 | mockOfDispose = new Mock(); 24 | mockOfBus = new Mock>(); 25 | mockOfBus.Setup(t => t.Subscribe(It.IsAny(), It.IsAny>())).Returns(this.mockOfDispose.Object); 26 | 27 | notifKey = fixture.Create(); 28 | } 29 | 30 | [Fact] 31 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 32 | public void WhenCtorWithoutBusBadArgs_ShouldThrowArgumentNullException() 33 | { 34 | Assert.Throws(() => { var monitor = new RedisChangeMonitor(null, notifKey); }); 35 | Assert.Throws(() => { var monitor = new RedisChangeMonitor(this.Bus, null); }); 36 | } 37 | 38 | [Fact] 39 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 40 | public void WhenCtor_ShouldHaveUniqueId() 41 | { 42 | var monitor1 = new RedisChangeMonitor(this.Bus, notifKey); 43 | Assert.NotNull(monitor1); 44 | Assert.True(monitor1.UniqueId.Length > 0); 45 | 46 | var monitor2 = new RedisChangeMonitor(this.Bus, notifKey); 47 | Assert.NotNull(monitor2); 48 | Assert.True(monitor2.UniqueId.Length > 0); 49 | 50 | Assert.NotSame(monitor1.UniqueId, monitor2.UniqueId); 51 | } 52 | 53 | [Fact] 54 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 55 | public void WhenCtor_ShouldBeRegistered() 56 | { 57 | var monitor = new RedisChangeMonitor(this.Bus, notifKey); 58 | this.mockOfBus.Verify(e => e.Subscribe(notifKey, monitor), Times.Once); 59 | } 60 | 61 | [Fact] 62 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 63 | public void WhenExceptionInCtor_ShouldBeDisposed() 64 | { 65 | this.mockOfBus.Setup(e => e.Subscribe(It.IsAny(), It.IsAny>())).Throws(); 66 | var monitor = new RedisChangeMonitor(this.Bus, notifKey); 67 | 68 | Assert.True(monitor.IsDisposed); 69 | } 70 | 71 | [Fact] 72 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 73 | public void WhenChanged_ShouldBeDisposed() 74 | { 75 | var monitor = new RedisChangeMonitor(this.Bus, notifKey); 76 | monitor.Notify(notifKey); 77 | 78 | Assert.True(monitor.IsDisposed); 79 | 80 | this.mockOfBus.Verify(e => e.Subscribe(notifKey, monitor), Times.Once); 81 | this.mockOfDispose.Verify(e => e.Dispose(), Times.Once); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("RedisMemoryCacheInvalidation.Test")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("RedisMemoryCacheInvalidation.Test")] 13 | [assembly: AssemblyCopyright("Copyright © 2013")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("1e894db6-2cef-4436-a139-43fc97f3f3c1")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.14474.12")] 36 | [assembly: AssemblyFileVersion("1.0.14474.12")] 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Redis/ExistingRedisConnectionTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using Ploeh.AutoFixture; 3 | using RedisMemoryCacheInvalidation.Redis; 4 | using StackExchange.Redis; 5 | using System; 6 | using System.Collections.Generic; 7 | using Xunit; 8 | 9 | namespace RedisMemoryCacheInvalidation.Tests.Redis 10 | { 11 | public class ExistingRedisConnectionTests 12 | { 13 | private Mock mockOfMux; 14 | private Mock mockOfSubscriber; 15 | private IRedisConnection cnx; 16 | private Fixture fixture = new Fixture(); 17 | 18 | public ExistingRedisConnectionTests() 19 | { 20 | //mock of subscriber 21 | mockOfSubscriber = new Mock(); 22 | mockOfSubscriber.Setup(s => s.UnsubscribeAll(It.IsAny())); 23 | mockOfSubscriber.Setup(s => s.Subscribe(It.IsAny(), It.IsAny>(), It.IsAny())); 24 | mockOfSubscriber.Setup(s => s.PublishAsync(It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(10L); 25 | //mock of mux 26 | mockOfMux = new Mock(); 27 | mockOfMux.Setup(c => c.IsConnected).Returns(true); 28 | mockOfMux.Setup(c => c.Close(false)); 29 | mockOfMux.Setup(c => c.GetSubscriber(It.IsAny())).Returns(this.mockOfSubscriber.Object); 30 | 31 | cnx = new ExistingRedisConnnection(mockOfMux.Object); 32 | } 33 | 34 | [Fact] 35 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 36 | public void WhenNotConnected_ShouldDoNothing() 37 | { 38 | this.mockOfMux.Setup(c => c.IsConnected).Returns(false); 39 | 40 | //connected 41 | var connected = cnx.Connect(); 42 | Assert.False(connected); 43 | 44 | //subscribe 45 | cnx.Subscribe("channel", (c, v) => { }) ; 46 | 47 | //getconfig 48 | var config = cnx.GetConfigAsync().Result; 49 | Assert.Equal[]>(new KeyValuePair[] { }, config); 50 | 51 | //publish 52 | var published = cnx.PublishAsync("channel", "value").Result; 53 | Assert.Equal(0L, published); 54 | 55 | cnx.UnsubscribeAll(); 56 | cnx.Disconnect(); 57 | 58 | mockOfMux.Verify(c => c.IsConnected, Times.AtLeastOnce); 59 | mockOfMux.Verify(c => c.GetSubscriber(null), Times.Never); 60 | mockOfMux.Verify(c => c.Close(It.IsAny()), Times.Never); 61 | } 62 | 63 | [Fact] 64 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 65 | public void WhenConnect_ShouldCheck_MuxIsConnected() 66 | { 67 | var connected = cnx.Connect(); 68 | Assert.True(connected); 69 | 70 | mockOfMux.Verify(c => c.IsConnected, Times.Once); 71 | 72 | Assert.True(cnx.IsConnected); 73 | } 74 | 75 | [Fact] 76 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 77 | public void WhenDisconnect_ShouldUnsubscribeAll() 78 | { 79 | cnx.Disconnect(); 80 | 81 | mockOfSubscriber.Verify(m => m.UnsubscribeAll(It.IsAny()), Times.Once); 82 | } 83 | 84 | [Fact] 85 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 86 | public void WhenPublishAsync_ShouldPublishMessages() 87 | { 88 | var channel = fixture.Create(); 89 | var value = fixture.Create(); 90 | 91 | var published = cnx.PublishAsync(channel, value).Result; 92 | 93 | Assert.Equal(10L, published); 94 | mockOfSubscriber.Verify(m => m.PublishAsync(channel, value, It.IsAny()), Times.Once); 95 | } 96 | 97 | [Fact] 98 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 99 | public void WhenSubscribe_ShouldSubscribe() 100 | { 101 | var channel = fixture.Create(); 102 | Action action = (c, v) => { }; 103 | 104 | cnx.Subscribe(channel, action); 105 | 106 | mockOfSubscriber.Verify(s => s.Subscribe(channel, action, It.IsAny()), Times.Once); 107 | } 108 | 109 | [Fact] 110 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 111 | public void WhenUnsubscribeAll_ShouldUnsubscribeAll() 112 | { 113 | cnx.UnsubscribeAll(); 114 | 115 | mockOfSubscriber.Verify(s => s.UnsubscribeAll(It.IsAny()), Times.Once); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Redis/RedisConnectionFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using Moq; 2 | using RedisMemoryCacheInvalidation.Redis; 3 | using StackExchange.Redis; 4 | using Xunit; 5 | 6 | namespace RedisMemoryCacheInvalidation.Tests.Redis 7 | { 8 | public class RedisConnectionFactoryTests 9 | { 10 | [Fact] 11 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 12 | public void WhenNewWithConfigOptions_Should_Create_StandaloneRedisConnection() 13 | { 14 | var cnx = RedisConnectionFactory.New("local:6379"); 15 | 16 | Assert.IsType(cnx); 17 | } 18 | 19 | [Fact] 20 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 21 | public void WhenNewWithMux_Should_Create_ExistingRedisConnection() 22 | { 23 | var mockOfMux = new Mock(); 24 | var cnx = RedisConnectionFactory.New(mockOfMux.Object); 25 | 26 | Assert.IsType(cnx); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/Redis/StandaloneRedisConnectionTests.cs: -------------------------------------------------------------------------------- 1 | using RedisMemoryCacheInvalidation.Redis; 2 | using StackExchange.Redis; 3 | using Xunit; 4 | 5 | namespace RedisMemoryCacheInvalidation.Tests.Redis 6 | { 7 | public class StandaloneRedisConnectionTests 8 | { 9 | [Fact] 10 | [Trait(TestConstants.TestCategory, TestConstants.UnitTestCategory)] 11 | public void WhenInvalidHost_Should_Not_Be_Connected() 12 | { 13 | var cnx = new StandaloneRedisConnection("local:6379"); 14 | 15 | var connected = cnx.Connect(); 16 | 17 | Assert.False(connected); 18 | Assert.False(cnx.IsConnected); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/RedisMemoryCacheInvalidation.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Debug 7 | AnyCPU 8 | {40A11C38-D401-47D2-87A7-35F4019D496B} 9 | Library 10 | Properties 11 | RedisMemoryCacheInvalidation.Tests 12 | RedisMemoryCacheInvalidation.Tests 13 | v4.5 14 | 512 15 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 16 | 10.0 17 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 18 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 19 | False 20 | UnitTest 21 | ..\..\ 22 | true 23 | 24 | 25 | 26 | 27 | 28 | true 29 | full 30 | false 31 | bin\Debug\ 32 | DEBUG;TRACE 33 | prompt 34 | 4 35 | false 36 | 37 | 38 | pdbonly 39 | true 40 | bin\Release\ 41 | TRACE 42 | prompt 43 | 4 44 | false 45 | 46 | 47 | 48 | ..\..\packages\Moq.4.2.1507.0118\lib\net40\Moq.dll 49 | True 50 | 51 | 52 | ..\..\packages\AutoFixture.3.31.3\lib\net40\Ploeh.AutoFixture.dll 53 | True 54 | 55 | 56 | ..\..\packages\StackExchange.Redis.1.0.481\lib\net45\StackExchange.Redis.dll 57 | True 58 | 59 | 60 | 61 | 62 | 63 | ..\..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll 64 | True 65 | 66 | 67 | ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll 68 | True 69 | 70 | 71 | ..\..\packages\xunit.extensibility.core.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.core.dll 72 | True 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | {13467724-727b-44ca-be9d-19bfc4599b34} 102 | RedisMemoryCacheInvalidation 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | False 115 | 116 | 117 | False 118 | 119 | 120 | False 121 | 122 | 123 | False 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 141 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/TestConstants.cs: -------------------------------------------------------------------------------- 1 | namespace RedisMemoryCacheInvalidation.Tests 2 | { 3 | public class TestConstants 4 | { 5 | public const string TestCategory = "Category"; 6 | public const string UnitTestCategory = "Unit"; 7 | public const string IntegrationTestCategory = "Integration"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/RedisMemoryCacheInvalidation.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tools/Redis Release Notes.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/tools/Redis Release Notes.docx -------------------------------------------------------------------------------- /tools/Redis on Windows.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/tools/Redis on Windows.docx -------------------------------------------------------------------------------- /tools/RedisService.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/tools/RedisService.docx -------------------------------------------------------------------------------- /tools/redis-benchmark.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/tools/redis-benchmark.exe -------------------------------------------------------------------------------- /tools/redis-check-aof.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/tools/redis-check-aof.exe -------------------------------------------------------------------------------- /tools/redis-check-dump.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/tools/redis-check-dump.exe -------------------------------------------------------------------------------- /tools/redis-cli.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/tools/redis-cli.exe -------------------------------------------------------------------------------- /tools/redis-server.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cybermaxs/RedisMemoryCacheInvalidation/39f0bbb91de0b1e8fd0a42f0340e7d74c18e0178/tools/redis-server.exe --------------------------------------------------------------------------------