├── .nuget ├── NuGet.exe ├── NuGet.Config └── NuGet.targets ├── CacheSleeve.Tests ├── TestObjects │ ├── Fruit.cs │ ├── Banana.cs │ └── Monkey.cs ├── packages.config ├── TestSettings.cs ├── Properties │ └── AssemblyInfo.cs ├── CacheManagerTests.cs ├── CacheSleeve.Tests.csproj ├── HybridCacherTests.cs ├── HybridCacherAsyncTests.cs ├── HttpContextCacherTests.cs ├── RedisCacherTests.cs └── RedisCacherAsyncTests.cs ├── CacheSleeve.NET40 ├── IObjectSerializer.cs ├── Models │ ├── Overview.cs │ └── Key.cs ├── packages.config ├── ICacheManager.cs ├── app.config ├── Utilities │ └── UnitTestDetector.cs ├── JsonObjectSerializer.cs ├── Properties │ └── AssemblyInfo.cs ├── Razor │ └── Overview.cshtml ├── ICacher.cs ├── HybridCacher.cs ├── HttpContextCacher.cs ├── CacheManager.cs ├── CacheSleeve.NET40.csproj └── RedisCacher.cs ├── CacheSleeve ├── packages.config ├── app.config ├── Properties │ └── AssemblyInfo.cs ├── CacheManagerAsync.cs ├── IAsyncCacher.cs ├── HybridCacherAsync.cs ├── CacheSleeve.csproj └── RedisCacherAsync.cs ├── CacheSleeve.StrongName.nuspec ├── CacheSleeve.nuspec ├── CacheSleeve.sln ├── .gitignore ├── README.md └── LICENSE /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jdehlin/CacheSleeve/HEAD/.nuget/NuGet.exe -------------------------------------------------------------------------------- /CacheSleeve.Tests/TestObjects/Fruit.cs: -------------------------------------------------------------------------------- 1 | namespace CacheSleeve.Tests.TestObjects 2 | { 3 | public abstract class Fruit 4 | { 5 | public string Color { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CacheSleeve.NET40/IObjectSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace CacheSleeve 2 | { 3 | public interface IObjectSerializer 4 | { 5 | T DeserializeObject(string serializedObj); 6 | string SerializeObject(object obj); 7 | } 8 | } -------------------------------------------------------------------------------- /CacheSleeve.NET40/Models/Overview.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | 4 | namespace CacheSleeve.Models 5 | { 6 | public class Overview 7 | { 8 | public IEnumerable RemoteKeys { get; set; } 9 | public IEnumerable LocalKeys { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /CacheSleeve.Tests/TestObjects/Banana.cs: -------------------------------------------------------------------------------- 1 | namespace CacheSleeve.Tests.TestObjects 2 | { 3 | public class Banana : Fruit 4 | { 5 | public Banana(int length, string color) 6 | { 7 | Length = length; 8 | Color = color; 9 | } 10 | 11 | public int Length { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /CacheSleeve.Tests/TestObjects/Monkey.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace CacheSleeve.Tests.TestObjects 4 | { 5 | public class Monkey 6 | { 7 | public Monkey(string name) 8 | { 9 | Name = name; 10 | } 11 | 12 | public string Name { get; set; } 13 | public List Bananas { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /CacheSleeve/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /CacheSleeve.NET40/Models/Key.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CacheSleeve.Models 4 | { 5 | public class Key 6 | { 7 | public Key(string keyName, DateTime? expirationDate = null) 8 | { 9 | KeyName = keyName; 10 | ExpirationDate = expirationDate; 11 | } 12 | 13 | 14 | public string KeyName { get; private set; } 15 | public DateTime? ExpirationDate { get; private set; } 16 | } 17 | } -------------------------------------------------------------------------------- /CacheSleeve.NET40/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /CacheSleeve.NET40/ICacheManager.cs: -------------------------------------------------------------------------------- 1 | using StackExchange.Redis; 2 | using System.Collections.Generic; 3 | 4 | namespace CacheSleeve 5 | { 6 | public interface ICacheManager 7 | { 8 | string AddPrefix(string key); 9 | 10 | bool Debug { get; set; } 11 | 12 | string GenerateOverview(); 13 | 14 | IEnumerable GetAllKeys(string pattern = null); 15 | 16 | IDatabase GetDatebase(); 17 | 18 | string KeyPrefix { get; } 19 | 20 | HttpContextCacher LocalCacher { get; } 21 | 22 | int RedisDb { get; } 23 | 24 | ConfigurationOptions RedisConfiguration { get; } 25 | 26 | RedisCacher RemoteCacher { get; } 27 | } 28 | } -------------------------------------------------------------------------------- /CacheSleeve/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /CacheSleeve.NET40/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /CacheSleeve.NET40/Utilities/UnitTestDetector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CacheSleeve.Utilities 4 | { 5 | static class UnitTestDetector 6 | { 7 | private static readonly bool RunningFromXUnit; 8 | 9 | static UnitTestDetector() 10 | { 11 | foreach (var assem in AppDomain.CurrentDomain.GetAssemblies()) 12 | { 13 | if (assem.FullName.ToLowerInvariant().StartsWith("xunit")) 14 | { 15 | RunningFromXUnit = true; 16 | break; 17 | } 18 | } 19 | } 20 | 21 | public static bool IsRunningFromXunit 22 | { 23 | get { return RunningFromXUnit; } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /CacheSleeve.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /CacheSleeve.NET40/JsonObjectSerializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace CacheSleeve 4 | { 5 | public class JsonObjectSerializer : IObjectSerializer 6 | { 7 | private readonly JsonSerializerSettings _jsonSettings; 8 | 9 | public JsonObjectSerializer() 10 | { 11 | _jsonSettings = new JsonSerializerSettings 12 | { 13 | TypeNameHandling = TypeNameHandling.Objects 14 | }; 15 | } 16 | 17 | public T DeserializeObject(string serializedObj) 18 | { 19 | return JsonConvert.DeserializeObject(serializedObj, _jsonSettings); 20 | } 21 | 22 | public string SerializeObject(object obj) 23 | { 24 | return JsonConvert.SerializeObject(obj, _jsonSettings); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /CacheSleeve.Tests/TestSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CacheSleeve.Tests.TestObjects; 3 | using Xunit; 4 | using System.Diagnostics; 5 | 6 | 7 | [assembly: CollectionBehavior(DisableTestParallelization = true)] 8 | 9 | namespace CacheSleeve.Tests 10 | { 11 | public static class TestSettings 12 | { 13 | public static string RedisHost = "localhost"; 14 | public static int RedisPort = 6379; 15 | public static string RedisPassword = null; 16 | public static int RedisDb = 5; 17 | public static string KeyPrefix = "cs."; 18 | 19 | // don't mess with George.. you'll break a lot of tests 20 | public static Monkey George = new Monkey("George") { Bananas = new List { new Banana(4, "yellow") }}; 21 | 22 | static TestSettings() 23 | { 24 | StartRedis(); 25 | } 26 | 27 | public static bool _redisStarted = false; 28 | 29 | public static void StartRedis() 30 | { 31 | if (!_redisStarted) 32 | { 33 | Process.Start(@"..\..\..\packages\redis-64.3.0.503\tools\redis-server.exe"); 34 | _redisStarted = true; 35 | } 36 | } 37 | 38 | } 39 | } -------------------------------------------------------------------------------- /CacheSleeve.StrongName.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CacheSleeve.StrongName 5 | 0.1.1 6 | CacheSleeve.StrongName 7 | Jack Dehlin 8 | Jack Dehlin 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | https://github.com/jdehlin/CacheSleeve/ 11 | false 12 | 13 | CacheSleeve lets you easily set up distributed in-memory cache with a Redis backplane for .NET projects. 14 | 15 | 16 | 17 | 18 | 19 | Added ability to connect to multiple servers 20 | Switched to HttpRuntime.Cache 21 | 22 | Copyright Jack Dehlin 2015 23 | Redis Cache Caching Distributed 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /CacheSleeve.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CacheSleeve 5 | 1.4.19 6 | CacheSleeve 7 | Jack Dehlin 8 | Jack Dehlin 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | https://github.com/jdehlin/CacheSleeve/ 11 | false 12 | 13 | CacheSleeve lets you easily set up distributed in-memory cache with a Redis backplane for .NET projects. 14 | 15 | 16 | 17 | 18 | 19 | Added ability to connect to multiple servers 20 | Switched to HttpRuntime.Cache 21 | 22 | Copyright Jack Dehlin 2015 23 | Redis Cache Caching Distributed 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /CacheSleeve.NET40/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("CacheSleeve.NET40")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CacheSleeve.NET40")] 13 | [assembly: AssemblyCopyright("Copyright © 2015")] 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("f856c9bc-4492-46ff-bc03-179c8ea088ea")] 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.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /CacheSleeve.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("CacheSleeve.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("CacheSleeve.Tests")] 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("63cb033c-2abf-45d0-8188-da0a44f3656f")] 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.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /CacheSleeve/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using System.Resources; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("CacheSleeve")] 10 | [assembly: AssemblyDescription("CacheSleeve lets you easily set up distributed in-memory cache with a Redis backplane for .NET projects.")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("Jack Dehlin")] 13 | [assembly: AssemblyProduct("CacheSleeve")] 14 | [assembly: AssemblyCopyright("")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | 18 | // Setting ComVisible to false makes the types in this assembly not visible 19 | // to COM components. If you need to access a type in this assembly from 20 | // COM, set the ComVisible attribute to true on that type. 21 | [assembly: ComVisible(false)] 22 | 23 | // The following GUID is for the ID of the typelib if this project is exposed to COM 24 | [assembly: Guid("f300ee93-afc3-42ad-8736-76372bf6c157")] 25 | 26 | // Version information for an assembly consists of the following four values: 27 | // 28 | // Major Version 29 | // Minor Version 30 | // Build Number 31 | // Revision 32 | // 33 | // You can specify all the values or you can default the Build and Revision Numbers 34 | // by using the '*' as shown below: 35 | // [assembly: AssemblyVersion("1.0.*")] 36 | [assembly: AssemblyVersion("0.3.*")] 37 | [assembly: AssemblyFileVersion("0.3.*")] 38 | [assembly: NeutralResourcesLanguageAttribute("en-US")] 39 | -------------------------------------------------------------------------------- /CacheSleeve/CacheManagerAsync.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using StackExchange.Redis; 3 | 4 | namespace CacheSleeve 5 | { 6 | public sealed partial class CacheManager 7 | { 8 | public async static void InitMultiAsync(string hosts, string redisPassword = null, int redisDb = 0, string keyPrefix = "cs.", int timeoutMilli = 5000) 9 | { 10 | var configuration = 11 | ConfigurationOptions.Parse(hosts); 12 | configuration.AllowAdmin = true; 13 | configuration.Password = redisPassword; 14 | configuration.AbortOnConnectFail = false; 15 | configuration.ConnectTimeout = timeoutMilli; 16 | 17 | await InitAsync(configuration, redisDb, keyPrefix); 18 | } 19 | 20 | public async static Task InitAsync(string redisHost, int redisPort = 6379, string redisPassword = null, int redisDb = 0, string keyPrefix = "cs.", int timeoutMilli = 5000) 21 | { 22 | var configuration = 23 | ConfigurationOptions.Parse(string.Format("{0}:{1}", redisHost, redisPort)); 24 | configuration.AllowAdmin = true; 25 | configuration.Password = redisPassword; 26 | configuration.AbortOnConnectFail = false; 27 | configuration.ConnectTimeout = timeoutMilli; 28 | 29 | await InitAsync(configuration, redisDb, keyPrefix); 30 | } 31 | 32 | public async static Task InitAsync(ConfigurationOptions config, int redisDb = 0, string keyPrefix = "cs.") 33 | { 34 | PopulateSettings(config, redisDb, keyPrefix); 35 | 36 | Settings._redisConnection = ConnectionMultiplexer.Connect(config); 37 | 38 | // Setup pub/sub for cache syncing 39 | var subscriber = Settings._redisConnection.GetSubscriber(); 40 | var removeSubscription = subscriber.SubscribeAsync("cacheSleeve.remove.*", (redisChannel, value) => Settings.LocalCacher.Remove(GetString(value))); 41 | var flushSubscription = subscriber.SubscribeAsync("cacheSleeve.flush*", (redisChannel, value) => Settings.LocalCacher.FlushAll()); 42 | await Task.WhenAll(removeSubscription, flushSubscription); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /CacheSleeve.NET40/Razor/Overview.cshtml: -------------------------------------------------------------------------------- 1 | @using System 2 | 18 | 19 |
20 | 21 |
22 | 23 |

Remote Cache Items

24 | 25 |
26 |
Minutes Remaining
27 |
Key Name
28 |
29 | @foreach (var key in Model.RemoteKeys) 30 | { 31 |
32 |
33 | @if (key.ExpirationDate != null) 34 | { 35 | @Math.Round(((key.ExpirationDate.Value - DateTime.Now).TotalMinutes), 2) 36 | } 37 | else 38 | { 39 | No Expiration 40 | } 41 |
42 |
@key.KeyName
43 |
44 | } 45 | 46 |
47 | 48 |
49 | 50 |

Local Cache Items

51 | 52 |
53 |
Minutes Remaining
54 |
Key Name
55 |
56 | @foreach (var key in Model.LocalKeys) 57 | { 58 |
59 |
60 | @if (key.ExpirationDate != null) 61 | { 62 | @Math.Round(((key.ExpirationDate.Value - DateTime.Now).TotalMinutes), 2) 63 | } 64 | else 65 | { 66 | No Expiration 67 | } 68 |
69 |
@key.KeyName
70 |
71 | } 72 | 73 |
74 | 75 |
-------------------------------------------------------------------------------- /CacheSleeve.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheSleeve", "CacheSleeve\CacheSleeve.csproj", "{7F2143AD-8ADE-4CE2-9655-481765759447}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{A88BEFB4-9F79-4BC0-8150-0D7C9F388611}" 9 | ProjectSection(SolutionItems) = preProject 10 | .nuget\NuGet.Config = .nuget\NuGet.Config 11 | .nuget\NuGet.exe = .nuget\NuGet.exe 12 | .nuget\NuGet.targets = .nuget\NuGet.targets 13 | EndProjectSection 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheSleeve.Tests", "CacheSleeve.Tests\CacheSleeve.Tests.csproj", "{7AD4782D-8830-46F2-877C-F2A9BBD3FCAB}" 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheSleeve.NET40", "CacheSleeve.NET40\CacheSleeve.NET40.csproj", "{FEA590F2-506A-4E20-ABD0-C2C3804C4C1D}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {7F2143AD-8ADE-4CE2-9655-481765759447}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {7F2143AD-8ADE-4CE2-9655-481765759447}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {7F2143AD-8ADE-4CE2-9655-481765759447}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {7F2143AD-8ADE-4CE2-9655-481765759447}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {7AD4782D-8830-46F2-877C-F2A9BBD3FCAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {7AD4782D-8830-46F2-877C-F2A9BBD3FCAB}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {7AD4782D-8830-46F2-877C-F2A9BBD3FCAB}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {7AD4782D-8830-46F2-877C-F2A9BBD3FCAB}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {FEA590F2-506A-4E20-ABD0-C2C3804C4C1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {FEA590F2-506A-4E20-ABD0-C2C3804C4C1D}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {FEA590F2-506A-4E20-ABD0-C2C3804C4C1D}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {FEA590F2-506A-4E20-ABD0-C2C3804C4C1D}.Release|Any CPU.Build.0 = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(SolutionProperties) = preSolution 39 | HideSolutionNode = FALSE 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | 11 | [Dd]ebug/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | [Bb]in/ 16 | [Oo]bj/ 17 | 18 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 19 | !packages/*/build/ 20 | 21 | # MSTest test Results 22 | [Tt]est[Rr]esult*/ 23 | [Bb]uild[Ll]og.* 24 | 25 | *_i.c 26 | *_p.c 27 | *.ilk 28 | *.meta 29 | *.obj 30 | *.pch 31 | *.pdb 32 | *.pgc 33 | *.pgd 34 | *.rsp 35 | *.sbr 36 | *.tlb 37 | *.tli 38 | *.tlh 39 | *.tmp 40 | *.tmp_proj 41 | *.log 42 | *.vspscc 43 | *.vssscc 44 | .builds 45 | *.pidb 46 | *.log 47 | *.scc 48 | *.nupkg 49 | 50 | # Visual C++ cache files 51 | ipch/ 52 | *.aps 53 | *.ncb 54 | *.opensdf 55 | *.sdf 56 | *.cachefile 57 | 58 | # Visual Studio profiler 59 | *.psess 60 | *.vsp 61 | *.vspx 62 | 63 | # Guidance Automation Toolkit 64 | *.gpState 65 | 66 | # ReSharper is a .NET coding add-in 67 | _ReSharper*/ 68 | *.[Rr]e[Ss]harper 69 | 70 | # TeamCity is a build add-in 71 | _TeamCity* 72 | 73 | # DotCover is a Code Coverage Tool 74 | *.dotCover 75 | 76 | # NCrunch 77 | *.ncrunch* 78 | .*crunch*.local.xml 79 | 80 | # Installshield output folder 81 | [Ee]xpress/ 82 | 83 | # DocProject is a documentation generator add-in 84 | DocProject/buildhelp/ 85 | DocProject/Help/*.HxT 86 | DocProject/Help/*.HxC 87 | DocProject/Help/*.hhc 88 | DocProject/Help/*.hhk 89 | DocProject/Help/*.hhp 90 | DocProject/Help/Html2 91 | DocProject/Help/html 92 | 93 | # Click-Once directory 94 | publish/ 95 | 96 | # Publish Web Output 97 | *.Publish.xml 98 | *.pubxml 99 | 100 | # NuGet Packages Directory 101 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 102 | packages/ 103 | 104 | # Windows Azure Build Output 105 | csx 106 | *.build.csdef 107 | 108 | # Windows Store app package directory 109 | AppPackages/ 110 | 111 | # Others 112 | sql/ 113 | *.Cache 114 | ClientBin/ 115 | [Ss]tyle[Cc]op.* 116 | ~$* 117 | *~ 118 | *.dbmdl 119 | *.[Pp]ublish.xml 120 | *.pfx 121 | *.publishsettings 122 | 123 | # RIA/Silverlight projects 124 | Generated_Code/ 125 | 126 | # Backup & report files from converting an old project file to a newer 127 | # Visual Studio version. Backup files are not needed, because we have git ;-) 128 | _UpgradeReport_Files/ 129 | Backup*/ 130 | UpgradeLog*.XML 131 | UpgradeLog*.htm 132 | 133 | # SQL Server files 134 | App_Data/*.mdf 135 | App_Data/*.ldf 136 | 137 | # ========================= 138 | # Windows detritus 139 | # ========================= 140 | 141 | # Windows image file caches 142 | Thumbs.db 143 | ehthumbs.db 144 | 145 | # Folder config file 146 | Desktop.ini 147 | 148 | # Recycle Bin used on file shares 149 | $RECYCLE.BIN/ 150 | 151 | # Mac crap 152 | .DS_Store 153 | 154 | 155 | -------------------------------------------------------------------------------- /CacheSleeve.Tests/CacheManagerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Web; 4 | using StackExchange.Redis; 5 | using Xunit; 6 | using System.Linq; 7 | 8 | namespace CacheSleeve.Tests 9 | { 10 | public class CacheManagerTests : IDisposable 11 | { 12 | private HybridCacher _hybridCacher; 13 | private RedisCacher _remoteCacher; 14 | private HttpContextCacher _localCacher; 15 | private readonly CacheManager _cacheSleeve; 16 | 17 | private delegate void SubscriptionHitHandler(string key, string message); 18 | private event SubscriptionHitHandler SubscriptionHit; 19 | private void OnSubscriptionHit(string key, string message) 20 | { 21 | if (SubscriptionHit != null) 22 | SubscriptionHit(key, message); 23 | } 24 | 25 | public CacheManagerTests() 26 | { 27 | // have to fake an http context to use http context cache 28 | HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null)); 29 | 30 | CacheManager.Init(TestSettings.RedisHost, TestSettings.RedisPort, TestSettings.RedisPassword, TestSettings.RedisDb, TestSettings.KeyPrefix); 31 | _cacheSleeve = CacheManager.Settings; 32 | 33 | var configuration = 34 | ConfigurationOptions.Parse(string.Format("{0}:{1}", TestSettings.RedisHost, TestSettings.RedisPort)); 35 | configuration.AllowAdmin = true; 36 | var redisConnection = ConnectionMultiplexer.Connect(configuration); 37 | 38 | var subscriber = redisConnection.GetSubscriber(); 39 | subscriber.Subscribe("cacheSleeve.remove.*", (redisChannel, value) => OnSubscriptionHit(redisChannel, GetString(value))); 40 | subscriber.Subscribe("cacheSleeve.flush*", (redisChannel, value) => OnSubscriptionHit(redisChannel, "flush")); 41 | 42 | _hybridCacher = new HybridCacher(); 43 | _remoteCacher = _cacheSleeve.RemoteCacher; 44 | _localCacher = _cacheSleeve.LocalCacher; 45 | } 46 | 47 | 48 | [Fact] 49 | public void GeneratesOverview() 50 | { 51 | var result = _cacheSleeve.GenerateOverview(); 52 | Assert.False(string.IsNullOrWhiteSpace(result)); 53 | } 54 | 55 | [Fact] 56 | public void OverviewContainsKeys() 57 | { 58 | _remoteCacher.Set("key1", "value1", DateTime.Now.AddSeconds(30)); 59 | _localCacher.Set("key2", "value2", DateTime.Now.AddMinutes(5)); 60 | var result = _cacheSleeve.GenerateOverview(); 61 | Assert.Equal(1, result.Select((c, i) => result.Substring(i)).Count(sub => sub.StartsWith("key1"))); 62 | Assert.Equal(1, result.Select((c, i) => result.Substring(i)).Count(sub => sub.StartsWith("key2"))); 63 | } 64 | 65 | 66 | public void Dispose() 67 | { 68 | _hybridCacher.FlushAll(); 69 | _hybridCacher = null; 70 | _remoteCacher = null; 71 | _localCacher = null; 72 | } 73 | 74 | /// 75 | /// Converts a byte[] to a string. 76 | /// 77 | /// The bytes to convert. 78 | /// The resulting string. 79 | private static string GetString(byte[] bytes) 80 | { 81 | var buffer = Encoding.Convert(Encoding.GetEncoding("iso-8859-1"), Encoding.UTF8, bytes); 82 | return Encoding.UTF8.GetString(buffer, 0, bytes.Count()); 83 | } 84 | } 85 | } -------------------------------------------------------------------------------- /CacheSleeve.NET40/ICacher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CacheSleeve.Models; 4 | 5 | namespace CacheSleeve 6 | { 7 | /// 8 | /// Interface for cachers 9 | /// 10 | public interface ICacher 11 | { 12 | /// 13 | /// Fetchs an item from the cache. 14 | /// 15 | /// The type of the item being fetched. 16 | /// The key of the item to retrieve. 17 | /// 18 | /// The requested item or null. 19 | /// 20 | T Get(string key); 21 | 22 | /// 23 | /// Insert an item into the cache. 24 | /// 25 | /// The type of the item to be inserted. 26 | /// The key of the item being inserted. 27 | /// The value of the item being inserted. 28 | /// The key of the item that this item is a child of. 29 | /// A boolean indicating success or failure. 30 | /// 31 | /// This will overwrite the value at the existing key if one exists. 32 | /// 33 | bool Set(string key, T value, string parentKey = null); 34 | 35 | /// 36 | /// Insert an item into the cache. 37 | /// 38 | /// The type of the item to be inserted. 39 | /// The key of the item being inserted. 40 | /// The value of the item being inserted. 41 | /// The date and time that the item should expire. 42 | /// The key of the item that this item is a child of. 43 | /// 44 | /// A boolean indicating success or failure. 45 | /// 46 | /// 47 | /// This will overwrite the value at the existing key if one exists. 48 | /// 49 | bool Set(string key, T value, DateTime expiresAt, string parentKey = null); 50 | 51 | /// 52 | /// Insert an item into the cache. 53 | /// 54 | /// The type of the item to be inserted. 55 | /// The key of the item being inserted. 56 | /// The value of the item being inserted. 57 | /// The time span that the item should be valid for. 58 | /// The key of the item that this item is a child of. 59 | /// 60 | /// A boolean indicating success or failure. 61 | /// 62 | /// 63 | /// This will overwrite the value at the existing key if one exists. 64 | /// 65 | bool Set(string key, T value, TimeSpan expiresIn, string parentKey = null); 66 | 67 | /// 68 | /// Deletes the specified item from the cache. 69 | /// 70 | /// The key of the item to delete. 71 | /// 72 | /// A boolean indicating success or failure. 73 | /// 74 | bool Remove(string key); 75 | 76 | /// 77 | /// Clear the whole cache. 78 | /// 79 | void FlushAll(); 80 | 81 | /// 82 | /// Returns a list of all available keys 83 | /// 84 | IEnumerable GetAllKeys(); 85 | } 86 | } -------------------------------------------------------------------------------- /CacheSleeve/IAsyncCacher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using CacheSleeve.Models; 5 | 6 | namespace CacheSleeve 7 | { 8 | /// 9 | /// Interface for cachers 10 | /// 11 | public interface IAsyncCacher 12 | { 13 | /// 14 | /// Fetchs an item from the cache. 15 | /// 16 | /// The type of the item being fetched. 17 | /// The key of the item to retrieve. 18 | /// 19 | /// The requested item or null. 20 | /// 21 | Task GetAsync(string key); 22 | 23 | /// 24 | /// Insert an item into the cache. 25 | /// 26 | /// The type of the item to be inserted. 27 | /// The key of the item being inserted. 28 | /// The value of the item being inserted. 29 | /// The key of the item that this item is a child of. 30 | /// A boolean indicating success or failure. 31 | /// 32 | /// This will overwrite the value at the existing key if one exists. 33 | /// 34 | Task SetAsync(string key, T value, string parentKey = null); 35 | 36 | /// 37 | /// Insert an item into the cache. 38 | /// 39 | /// The type of the item to be inserted. 40 | /// The key of the item being inserted. 41 | /// The value of the item being inserted. 42 | /// The date and time that the item should expire. 43 | /// The key of the item that this item is a child of. 44 | /// 45 | /// A boolean indicating success or failure. 46 | /// 47 | /// 48 | /// This will overwrite the value at the existing key if one exists. 49 | /// 50 | Task SetAsync(string key, T value, DateTime expiresAt, string parentKey = null); 51 | 52 | /// 53 | /// Insert an item into the cache. 54 | /// 55 | /// The type of the item to be inserted. 56 | /// The key of the item being inserted. 57 | /// The value of the item being inserted. 58 | /// The time span that the item should be valid for. 59 | /// The key of the item that this item is a child of. 60 | /// 61 | /// A boolean indicating success or failure. 62 | /// 63 | /// 64 | /// This will overwrite the value at the existing key if one exists. 65 | /// 66 | Task SetAsync(string key, T value, TimeSpan expiresIn, string parentKey = null); 67 | 68 | /// 69 | /// Deletes the specified item from the cache. 70 | /// 71 | /// The key of the item to delete. 72 | /// 73 | /// A boolean indicating success or failure. 74 | /// 75 | Task RemoveAsync(string key); 76 | 77 | /// 78 | /// Clear the whole cache. 79 | /// 80 | Task FlushAllAsync(); 81 | 82 | /// 83 | /// Returns a list of all available keys 84 | /// 85 | Task> GetAllKeysAsync(); 86 | } 87 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CacheSleeve - Distributed In-Memory Caching for .NET 2 | =========== 3 | 4 | 5 | Overview 6 | -------- 7 | CacheSleeve lets you easily add 2-tier distributed in-memory caching to your ASP.NET projects. 8 | 9 | Using CacheSleeves HybridCacher you can simply add items to the cache and they will automatically be synced across all servers in your farm using a combination of Redis and in-memory caching. 10 | 11 | Uses [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis) for interacting with Redis. 12 | 13 | Features 14 | -------- 15 | ### Efficiency 16 | 17 | CacheSleeve primarily increases efficiency over non-distributed caching (i.e. each server has it's own isolated cache) by preventing each web server from having to fetch it's own cache items from the database. 18 | With CacheSleeve only one database call is made to populate the distributed cache. Once the item is in the distributed cache all servers will use that item until it is invalidated. 19 | 20 | The second way that CacheSleeve increases efficiency is by storing cache items in each web servers in-memory cache upon first request. This means once a web server has requested a cache item 21 | it will use its local in-memory cache until the item is invalidated in the distributed cache. 22 | 23 | 24 | ### Automatic Invalidation 25 | 26 | > There are only two hard things in Computer Science: cache invalidation and naming things. 27 | > -- Phil Karlton 28 | 29 | All cache invalidation is synced across all connected servers. Remove a cache item on one server and it will automatically be invalidated on all servers. 30 | 31 | This invalidation also works for keys which are invalidated due to an expiration date/time and for parent/child relationships. 32 | 33 | 34 | ### Cache Dependency 35 | 36 | Set parent/child relationships for cache items. Removing a parent item will invalidate its children across all connected servers. 37 | 38 | 39 | ### Cache Expiration 40 | 41 | Set a time span that cache items should live for. When the item expires it will be invalidated across all connected servers. 42 | 43 | 44 | ### Async Support 45 | 46 | All caching methods now have Async equivalents. 47 | 48 | 49 | Setup 50 | ----- 51 | ### Initialize the CacheManager 52 | 53 | Before you can start using the distributed cache you will need to initialize the CacheSleeve CacheManager with connection details for your Redis server. 54 | This should only be done once per server connecting to the distributed cache, usually in Application_Start. 55 | 56 | If you're running redis on localhost with default settings the following is all you need to get the cache manager running: 57 | 58 | ``` 59 | CacheSleeve.CacheManager.Init("localhost"); 60 | ``` 61 | 62 | Once the cache manager has been initialized You can use the HybridCacher to interact with the distributed cache. 63 | 64 | #### Server A 65 | ``` 66 | var cacher = new CacheSleeve.HybridCacher(); 67 | cacher.Set("key", "value"); 68 | ``` 69 | 70 | This item is now in the distributed cache and will be synced/invalidated across all connected servers. 71 | 72 | #### Server B 73 | ``` 74 | var cacher = new CacheSleeve.HybridCacher(); 75 | var item = cacher.Get("key"); 76 | ``` 77 | 78 | Here _item_ is equal to "value" and the cache item has also been stored in Server B's in-memory cache for subsequent requests. 79 | 80 | #### Server A (again) 81 | ``` 82 | cacher.Remove("key"); 83 | ``` 84 | 85 | This invalidates the item with the key "key" in Server A's in-memory cache, the distributed cache, Server B's in-memory cache, and any other connected servers in-memory cache. 86 | 87 | #### Server B (again) 88 | ``` 89 | var item = cacher.Get("key"); 90 | ``` 91 | 92 | Here _item_ is now null because it was invalidated on Server A. 93 | 94 | 95 | Examples 96 | -------- 97 | Examples of more advanced usage coming soon 98 | 99 | 100 | Dependency Injection 101 | -------------------- 102 | You can also use the HybridCacher without hard coding new instances into your code. HybridCacher implements the CacheSleeve.ICacher interface which you can use to inject the dependency. 103 | 104 | Example dependency setup (StructureMap): 105 | ``` 106 | For().Use(new HybridCacher()); 107 | ``` 108 | 109 | You can then use constructor injection (or property injection) to inject the dependency. -------------------------------------------------------------------------------- /CacheSleeve/HybridCacherAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CacheSleeve.Models; 6 | 7 | namespace CacheSleeve 8 | { 9 | public partial class HybridCacher : IAsyncCacher 10 | { 11 | public async Task GetAsync(string key) 12 | { 13 | var result = _localCacher.Get(key); 14 | if (result != null) 15 | return result; 16 | result = await _remoteCacher.GetAsync(key); 17 | if (result != null) 18 | { 19 | var ttl = (int)(await _remoteCacher.TimeToLiveAsync(key)); 20 | var parentKey = _remoteCacher.Get(key + ".parent"); 21 | if (parentKey != null) 22 | parentKey = parentKey.Substring(_cacheSleeve.KeyPrefix.Length); 23 | if (ttl > -1) 24 | _localCacher.Set(key, result, TimeSpan.FromSeconds(ttl), parentKey); 25 | else 26 | _localCacher.Set(key, result, parentKey); 27 | result = _localCacher.Get(key); 28 | } 29 | return result; 30 | } 31 | 32 | public async Task GetOrSetAsync(string key, Func> valueFactory, DateTime expiresAt, string parentKey = null) 33 | { 34 | var value = await GetAsync(key); 35 | if (value == null) 36 | { 37 | value = await valueFactory(key); 38 | if (value != null && !value.Equals(default(T))) 39 | await SetAsync(key, value, expiresAt, parentKey); 40 | } 41 | return value; 42 | } 43 | 44 | public async Task SetAsync(string key, T value, string parentKey = null) 45 | { 46 | try 47 | { 48 | await _remoteCacher.SetAsync(key, value, parentKey); 49 | await _remoteCacher.PublishToKeyAsync("cacheSleeve.remove." + key, key); 50 | return true; 51 | } 52 | catch (Exception) 53 | { 54 | _localCacher.Remove(key); 55 | _remoteCacher.Remove(key); 56 | return false; 57 | } 58 | } 59 | 60 | public async Task SetAsync(string key, T value, DateTime expiresAt, string parentKey = null) 61 | { 62 | try 63 | { 64 | await _remoteCacher.SetAsync(key, value, expiresAt, parentKey); 65 | await _remoteCacher.PublishToKeyAsync("cacheSleeve.remove." + key, key); 66 | return true; 67 | } 68 | catch (Exception) 69 | { 70 | _localCacher.Remove(key); 71 | _remoteCacher.Remove(key); 72 | return false; 73 | } 74 | } 75 | 76 | public async Task SetAsync(string key, T value, TimeSpan expiresIn, string parentKey = null) 77 | { 78 | try 79 | { 80 | await _remoteCacher.SetAsync(key, value, expiresIn, parentKey); 81 | await _remoteCacher.PublishToKeyAsync("cacheSleeve.remove." + key, key); 82 | return true; 83 | } 84 | catch (Exception) 85 | { 86 | _localCacher.Remove(key); 87 | _remoteCacher.Remove(key); 88 | return false; 89 | } 90 | } 91 | 92 | public async Task RemoveAsync(string key) 93 | { 94 | try 95 | { 96 | await _remoteCacher.RemoveAsync(key); 97 | await _remoteCacher.PublishToKeyAsync("cacheSleeve.remove." + key, key); 98 | return true; 99 | } 100 | catch (Exception) 101 | { 102 | return false; 103 | } 104 | } 105 | 106 | public async Task FlushAllAsync() 107 | { 108 | await _remoteCacher.FlushAllAsync(); 109 | await _remoteCacher.PublishToKeyAsync("cacheSleeve.flush", ""); 110 | } 111 | 112 | public async Task> GetAllKeysAsync() 113 | { 114 | var keys = await _remoteCacher.GetAllKeysAsync(); 115 | keys = keys.Union(_localCacher.GetAllKeys()); 116 | return keys.GroupBy(k => k.KeyName).Select(grp => grp.First()); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /CacheSleeve.NET40/HybridCacher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using CacheSleeve.Models; 5 | 6 | namespace CacheSleeve 7 | { 8 | public partial class HybridCacher : ICacher 9 | { 10 | private readonly CacheManager _cacheSleeve; 11 | private readonly RedisCacher _remoteCacher; 12 | private readonly HttpContextCacher _localCacher; 13 | 14 | public HybridCacher() 15 | { 16 | _cacheSleeve = CacheManager.Settings; 17 | 18 | _remoteCacher = _cacheSleeve.RemoteCacher; 19 | _localCacher = _cacheSleeve.LocalCacher; 20 | } 21 | 22 | 23 | public T Get(string key) 24 | { 25 | var result = _localCacher.Get(key); 26 | if (result != null) 27 | return result; 28 | result = _remoteCacher.Get(key); 29 | if (result != null) 30 | { 31 | var ttl = _remoteCacher.TimeToLive(key); 32 | var parentKey = _remoteCacher.Get(key + ".parent"); 33 | if (parentKey != null) 34 | parentKey = parentKey.Substring(_cacheSleeve.KeyPrefix.Length); 35 | if (ttl > -1) 36 | _localCacher.Set(key, result, TimeSpan.FromSeconds(ttl), parentKey); 37 | else 38 | _localCacher.Set(key, result, parentKey); 39 | result = _localCacher.Get(key); 40 | } 41 | return result; 42 | } 43 | 44 | public T GetOrSet(string key, Func valueFactory, DateTime expiresAt, string parentKey = null) 45 | { 46 | var value = Get(key); 47 | if (value == null) 48 | { 49 | value = valueFactory(key); 50 | if (value != null && !value.Equals(default(T))) 51 | Set(key, value, expiresAt, parentKey); 52 | } 53 | return value; 54 | } 55 | 56 | public bool Set(string key, T value, string parentKey = null) 57 | { 58 | try 59 | { 60 | _remoteCacher.Set(key, value, parentKey); 61 | _remoteCacher.PublishToKey("cacheSleeve.remove." + key, key); 62 | return true; 63 | } 64 | catch (Exception) 65 | { 66 | _localCacher.Remove(key); 67 | _remoteCacher.Remove(key); 68 | return false; 69 | } 70 | } 71 | 72 | public bool Set(string key, T value, DateTime expiresAt, string parentKey = null) 73 | { 74 | try 75 | { 76 | _remoteCacher.Set(key, value, expiresAt, parentKey); 77 | _remoteCacher.PublishToKey("cacheSleeve.remove." + key, key); 78 | return true; 79 | } 80 | catch (Exception) 81 | { 82 | _localCacher.Remove(key); 83 | _remoteCacher.Remove(key); 84 | return false; 85 | } 86 | } 87 | 88 | public bool Set(string key, T value, TimeSpan expiresIn, string parentKey = null) 89 | { 90 | try 91 | { 92 | _remoteCacher.Set(key, value, expiresIn, parentKey); 93 | _remoteCacher.PublishToKey("cacheSleeve.remove." + key, key); 94 | return true; 95 | } 96 | catch (Exception) 97 | { 98 | _localCacher.Remove(key); 99 | _remoteCacher.Remove(key); 100 | return false; 101 | } 102 | } 103 | 104 | public bool Remove(string key) 105 | { 106 | try 107 | { 108 | _remoteCacher.Remove(key); 109 | _remoteCacher.PublishToKey("cacheSleeve.remove." + key, key); 110 | return true; 111 | } 112 | catch (Exception) 113 | { 114 | return false; 115 | } 116 | } 117 | 118 | public void FlushAll() 119 | { 120 | _remoteCacher.FlushAll(); 121 | _remoteCacher.PublishToKey("cacheSleeve.flush", ""); 122 | } 123 | 124 | public IEnumerable GetAllKeys() 125 | { 126 | var keys = _remoteCacher.GetAllKeys(); 127 | keys = keys.Union(_localCacher.GetAllKeys()); 128 | return keys; 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /CacheSleeve.NET40/HttpContextCacher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Web.Caching; 7 | using CacheSleeve.Models; 8 | 9 | namespace CacheSleeve 10 | { 11 | public class HttpContextCacher : ICacher 12 | { 13 | private readonly Cache _cache; 14 | private readonly CacheManager _cacheSleeve; 15 | 16 | public HttpContextCacher() 17 | { 18 | _cache = System.Web.HttpRuntime.Cache; 19 | _cacheSleeve = CacheManager.Settings; 20 | } 21 | 22 | 23 | public T Get(string key) 24 | { 25 | var cacheEntry = (CacheEntry)_cache.Get(_cacheSleeve.AddPrefix(key)); 26 | if (cacheEntry != null) 27 | return (T)cacheEntry.Value; 28 | return default(T); 29 | } 30 | 31 | public bool Set(string key, T value, string parentKey = null) 32 | { 33 | var entry = new CacheEntry(value, null); 34 | return InternalSet(key, entry, parentKey); 35 | } 36 | 37 | public bool Set(string key, T value, DateTime expiresAt, string parentKey = null) 38 | { 39 | var entry = new CacheEntry(value, expiresAt); 40 | return InternalSet(key, entry, parentKey); 41 | } 42 | 43 | public bool Set(string key, T value, TimeSpan expiresIn, string parentKey = null) 44 | { 45 | var entry = new CacheEntry(value, DateTime.Now.Add(expiresIn)); 46 | return InternalSet(key, entry, parentKey); 47 | } 48 | 49 | public bool Remove(string key) 50 | { 51 | if (_cache.Get(_cacheSleeve.AddPrefix(key)) == null) 52 | return false; 53 | try 54 | { 55 | _cache.Remove(_cacheSleeve.AddPrefix(key)); 56 | _cache.Remove(_cacheSleeve.AddPrefix(key + ".parent")); 57 | if (_cacheSleeve.Debug) 58 | Trace.WriteLine(string.Format("CS HttpContext: Removed cache item with key {0}", key)); 59 | return true; 60 | } 61 | catch (Exception) 62 | { 63 | return false; 64 | } 65 | } 66 | 67 | public void FlushAll() 68 | { 69 | var enumerator = _cache.GetEnumerator(); 70 | while (enumerator.MoveNext()) 71 | _cache.Remove(enumerator.Key.ToString()); 72 | } 73 | 74 | public IEnumerable GetAllKeys() 75 | { 76 | var keys = _cache.Cast() 77 | .Where(de => de.Value.GetType() == typeof(CacheEntry)) 78 | .Select(de => new Key((de.Key as string), (de.Value as CacheEntry).ExpiresAt)); 79 | return keys; 80 | } 81 | 82 | /// 83 | /// 84 | /// 85 | /// 86 | /// 87 | public int TimeToLive(string key) 88 | { 89 | var result = (CacheEntry)_cache.Get(_cacheSleeve.AddPrefix(key)); 90 | if (result == null || result.ExpiresAt == null) 91 | return -1; 92 | return (int)(result.ExpiresAt.Value - DateTime.Now).TotalSeconds; 93 | } 94 | 95 | /// 96 | /// Shared insert for public wrappers. 97 | /// 98 | /// The key of the item to insert. 99 | /// The internal CacheEntry object to insert. 100 | /// The key of the item that this item is a child of. 101 | private bool InternalSet(string key, CacheEntry entry, string parentKey = null) 102 | { 103 | CacheDependency cacheDependency = null; 104 | if (!string.IsNullOrWhiteSpace(parentKey)) 105 | cacheDependency = new CacheDependency(null, new[] { _cacheSleeve.AddPrefix(parentKey) }); 106 | try 107 | { 108 | if (entry.ExpiresAt == null) 109 | _cache.Insert(_cacheSleeve.AddPrefix(key), entry, cacheDependency); 110 | else 111 | _cache.Insert(_cacheSleeve.AddPrefix(key), entry, cacheDependency, entry.ExpiresAt.Value.ToUniversalTime(), Cache.NoSlidingExpiration); 112 | if (_cacheSleeve.Debug) 113 | Trace.WriteLine(string.Format("CS HttpContext: Set cache item with key {0}", key)); 114 | return true; 115 | } 116 | catch (Exception) 117 | { 118 | return false; 119 | } 120 | } 121 | 122 | /// 123 | /// Private class for the wrapper around the cache items. 124 | /// 125 | private class CacheEntry 126 | { 127 | /// 128 | /// Creates a new instance of CacheEntry. 129 | /// 130 | /// The value being cached. 131 | /// The UTC time at which CacheEntry expires. 132 | public CacheEntry(object value, DateTime? expiresAt) 133 | { 134 | Value = value; 135 | ExpiresAt = expiresAt; 136 | } 137 | 138 | /// 139 | /// UTC time at which CacheEntry expires. 140 | /// 141 | internal DateTime? ExpiresAt { get; private set; } 142 | 143 | /// 144 | /// The value that is cached. 145 | /// 146 | internal object Value { get; private set; } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /CacheSleeve/CacheSleeve.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {7F2143AD-8ADE-4CE2-9655-481765759447} 8 | Library 9 | Properties 10 | CacheSleeve 11 | CacheSleeve 12 | v4.5 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 | False 39 | ..\packages\Newtonsoft.Json.5.0.8\lib\net45\Newtonsoft.Json.dll 40 | 41 | 42 | False 43 | ..\packages\RazorEngine.3.6.0\lib\net45\RazorEngine.dll 44 | 45 | 46 | False 47 | ..\packages\StackExchange.Redis.1.0.394\lib\net45\StackExchange.Redis.dll 48 | 49 | 50 | 51 | 52 | 53 | False 54 | ..\packages\Microsoft.AspNet.Razor.3.0.0\lib\net45\System.Web.Razor.dll 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | CacheManager.cs 65 | 66 | 67 | HttpContextCacher.cs 68 | 69 | 70 | HybridCacher.cs 71 | 72 | 73 | ICacheManager.cs 74 | 75 | 76 | ICacher.cs 77 | 78 | 79 | IObjectSerializer.cs 80 | 81 | 82 | JsonObjectSerializer.cs 83 | 84 | 85 | Models\Key.cs 86 | 87 | 88 | Models\Overview.cs 89 | 90 | 91 | RedisCacher.cs 92 | 93 | 94 | Utilities\UnitTestDetector.cs 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | Razor\Overview.cshtml 105 | Always 106 | 107 | 108 | 109 | 110 | 111 | 112 | 119 | -------------------------------------------------------------------------------- /CacheSleeve.Tests/CacheSleeve.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Debug 7 | AnyCPU 8 | {7AD4782D-8830-46F2-877C-F2A9BBD3FCAB} 9 | Library 10 | Properties 11 | CacheSleeve.Tests 12 | CacheSleeve.Tests 13 | v4.5 14 | 512 15 | ..\ 16 | true 17 | 18 | 19 | 20 | 21 | true 22 | full 23 | false 24 | bin\Debug\ 25 | DEBUG;TRACE 26 | prompt 27 | 4 28 | 29 | 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 37 | 38 | 39 | False 40 | ..\packages\StackExchange.Redis.1.0.394\lib\net45\StackExchange.Redis.dll 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll 52 | True 53 | 54 | 55 | ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll 56 | True 57 | 58 | 59 | ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll 60 | True 61 | 62 | 63 | ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll 64 | True 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | Designer 83 | 84 | 85 | 86 | 87 | {7f2143ad-8ade-4ce2-9655-481765759447} 88 | CacheSleeve 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 99 | 100 | 101 | 102 | 109 | -------------------------------------------------------------------------------- /CacheSleeve.NET40/CacheManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Linq; 5 | using CacheSleeve.Models; 6 | using RazorEngine; 7 | using StackExchange.Redis; 8 | using Encoding = System.Text.Encoding; 9 | 10 | namespace CacheSleeve 11 | { 12 | public sealed partial class CacheManager : ICacheManager 13 | { 14 | private ConnectionMultiplexer _redisConnection; 15 | 16 | #region Singleton Setup 17 | 18 | private CacheManager() 19 | { 20 | } 21 | 22 | public static CacheManager Settings 23 | { 24 | get { return Nested.Settings; } 25 | } 26 | 27 | private class Nested 28 | { 29 | static Nested() 30 | { 31 | } 32 | 33 | internal static readonly CacheManager Settings = new CacheManager(); 34 | } 35 | 36 | #endregion 37 | 38 | 39 | public static void InitMulti(string hosts, string redisPassword = null, int redisDb = 0, string keyPrefix = "cs.", int timeoutMilli = 5000) 40 | { 41 | var configuration = 42 | ConfigurationOptions.Parse(hosts); 43 | configuration.AllowAdmin = true; 44 | configuration.Password = redisPassword; 45 | configuration.AbortOnConnectFail = false; 46 | configuration.ConnectTimeout = timeoutMilli; 47 | 48 | Init(configuration, redisDb, keyPrefix); 49 | } 50 | 51 | public static void Init(string redisHost, int redisPort = 6379, string redisPassword = null, int redisDb = 0, string keyPrefix = "cs.", int timeoutMilli = 5000) 52 | { 53 | var configuration = 54 | ConfigurationOptions.Parse(string.Format("{0}:{1}", redisHost, redisPort)); 55 | configuration.AllowAdmin = true; 56 | configuration.Password = redisPassword; 57 | configuration.AbortOnConnectFail = false; 58 | configuration.ConnectTimeout = timeoutMilli; 59 | 60 | Init(configuration, redisDb, keyPrefix); 61 | } 62 | 63 | public static void Init(ConfigurationOptions config, int redisDb = 0, string keyPrefix = "cs.") 64 | { 65 | PopulateSettings(config, redisDb, keyPrefix); 66 | 67 | Settings._redisConnection = ConnectionMultiplexer.Connect(config); 68 | 69 | // Setup pub/sub for cache syncing 70 | var subscriber = Settings._redisConnection.GetSubscriber(); 71 | subscriber.Subscribe("cacheSleeve.remove.*", (redisChannel, value) => Settings.LocalCacher.Remove(GetString(value))); 72 | subscriber.Subscribe("cacheSleeve.flush*", (redisChannel, value) => Settings.LocalCacher.FlushAll()); 73 | } 74 | 75 | 76 | private static void PopulateSettings(ConfigurationOptions config, int redisDb = 0, string keyPrefix = "cs.") 77 | { 78 | Settings.RedisConfiguration = config; 79 | Settings.KeyPrefix = keyPrefix; 80 | Settings.RedisDb = redisDb; 81 | 82 | Settings.RemoteCacher = new RedisCacher(); 83 | Settings.LocalCacher = new HttpContextCacher(); 84 | } 85 | 86 | public string GenerateOverview() 87 | { 88 | const string resourceName = "CacheSleeve.Razor.Overview.cshtml"; 89 | var model = new Overview 90 | { 91 | RemoteKeys = RemoteCacher.GetAllKeys(), 92 | LocalKeys = LocalCacher.GetAllKeys() 93 | }; 94 | var assembly = Assembly.GetExecutingAssembly(); 95 | using (var stream = assembly.GetManifestResourceStream(resourceName)) 96 | { 97 | if (stream == null) 98 | return ""; 99 | using (var reader = new StreamReader(stream)) 100 | return Razor.Parse(reader.ReadToEnd(), model); 101 | } 102 | } 103 | 104 | /// 105 | /// If true then logging is enabled 106 | /// 107 | public bool Debug { get; set; } 108 | 109 | /// 110 | /// The out of band caching service used as a backplane to share cache across servers. 111 | /// 112 | public RedisCacher RemoteCacher { get; private set; } 113 | 114 | /// 115 | /// The local in-memory cache. 116 | /// 117 | public HttpContextCacher LocalCacher { get; private set; } 118 | 119 | /// 120 | /// The prefix added to keys of items cached by CacheSleeve to prevent collisions. 121 | /// 122 | public string KeyPrefix { get; private set; } 123 | 124 | /// 125 | /// Redis connection configuration. 126 | /// 127 | public ConfigurationOptions RedisConfiguration { get; private set; } 128 | 129 | /// 130 | /// The database to use on the Redis server. 131 | /// 132 | public int RedisDb { get; private set; } 133 | 134 | public IDatabase GetDatebase() 135 | { 136 | return _redisConnection.GetDatabase(RedisDb); 137 | } 138 | 139 | public IEnumerable GetAllKeys(string pattern = null) 140 | { 141 | var keys = new List(); 142 | foreach (var endpoint in _redisConnection.GetEndPoints()) 143 | { 144 | var server = _redisConnection.GetServer(endpoint); 145 | if (!server.IsSlave && server.IsConnected) 146 | keys.AddRange(server.Keys(database: Settings.RedisDb, pattern: pattern != null ? Settings.AddPrefix(pattern) : Settings.AddPrefix("*"))); 147 | } 148 | return keys; 149 | } 150 | 151 | /// 152 | /// Adds the prefix to the key. 153 | /// 154 | /// The specified key value. 155 | /// The specified key with the prefix attached. 156 | public string AddPrefix(string key) 157 | { 158 | return string.Format("{0}{1}", KeyPrefix, key); 159 | } 160 | 161 | /// 162 | /// Converts a byte[] to a string. 163 | /// 164 | /// The bytes to convert. 165 | /// The resulting string. 166 | private static string GetString(byte[] bytes) 167 | { 168 | var buffer = Encoding.Convert(Encoding.GetEncoding("iso-8859-1"), Encoding.UTF8, bytes); 169 | return Encoding.UTF8.GetString(buffer, 0, bytes.Count()); 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /CacheSleeve.NET40/CacheSleeve.NET40.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {FEA590F2-506A-4E20-ABD0-C2C3804C4C1D} 8 | Library 9 | Properties 10 | CacheSleeve 11 | CacheSleeve 12 | v4.0 13 | 512 14 | ..\ 15 | true 16 | 17 | 724b2977 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 | 40 | 41 | ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.dll 42 | 43 | 44 | ..\packages\Microsoft.Bcl.Async.1.0.168\lib\net40\Microsoft.Threading.Tasks.Extensions.Desktop.dll 45 | 46 | 47 | False 48 | ..\packages\Newtonsoft.Json.5.0.8\lib\net40\Newtonsoft.Json.dll 49 | 50 | 51 | False 52 | ..\packages\RazorEngine.3.6.0\lib\net40\RazorEngine.dll 53 | 54 | 55 | False 56 | ..\packages\StackExchange.Redis.1.0.394\lib\net40\StackExchange.Redis.dll 57 | 58 | 59 | 60 | 61 | ..\packages\Microsoft.Bcl.1.1.9\lib\net40\System.IO.dll 62 | 63 | 64 | 65 | ..\packages\Microsoft.Bcl.1.1.9\lib\net40\System.Runtime.dll 66 | 67 | 68 | ..\packages\Microsoft.Bcl.1.1.9\lib\net40\System.Threading.Tasks.dll 69 | 70 | 71 | 72 | True 73 | ..\packages\Microsoft.AspNet.Razor.2.0.30506.0\lib\net40\System.Web.Razor.dll 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Always 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 109 | 110 | 111 | 112 | 113 | 114 | 121 | -------------------------------------------------------------------------------- /CacheSleeve.Tests/HybridCacherTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Web; 6 | using StackExchange.Redis; 7 | using Xunit; 8 | 9 | namespace CacheSleeve.Tests 10 | { 11 | public class HybridCacherTests : IDisposable 12 | { 13 | private HybridCacher _hybridCacher; 14 | private RedisCacher _remoteCacher; 15 | private HttpContextCacher _localCacher; 16 | private readonly CacheManager _cacheSleeve; 17 | 18 | private delegate void SubscriptionHitHandler(string key, string message); 19 | private event SubscriptionHitHandler SubscriptionHit; 20 | private void OnSubscriptionHit(string key, string message) 21 | { 22 | if (SubscriptionHit != null) 23 | SubscriptionHit(key, message); 24 | } 25 | 26 | public HybridCacherTests() 27 | { 28 | // have to fake an http context to use http context cache 29 | HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null)); 30 | 31 | CacheManager.Init(TestSettings.RedisHost, TestSettings.RedisPort, TestSettings.RedisPassword, TestSettings.RedisDb, TestSettings.KeyPrefix); 32 | _cacheSleeve = CacheManager.Settings; 33 | 34 | var cacheSleeve = CacheManager.Settings; 35 | 36 | var configuration = 37 | ConfigurationOptions.Parse(string.Format("{0}:{1}", TestSettings.RedisHost, TestSettings.RedisPort)); 38 | configuration.AllowAdmin = true; 39 | var redisConnection = ConnectionMultiplexer.Connect(configuration); 40 | 41 | var subscriber = redisConnection.GetSubscriber(); 42 | subscriber.Subscribe("cacheSleeve.remove.*", (redisChannel, value) => OnSubscriptionHit(redisChannel, GetString(value))); 43 | subscriber.Subscribe("cacheSleeve.flush*", (redisChannel, value) => OnSubscriptionHit(redisChannel, "flush")); 44 | 45 | _hybridCacher = new HybridCacher(); 46 | _remoteCacher = cacheSleeve.RemoteCacher; 47 | _localCacher = cacheSleeve.LocalCacher; 48 | } 49 | 50 | 51 | public class Basics : HybridCacherTests 52 | { 53 | [Fact] 54 | public void SetCachesRemote() 55 | { 56 | _hybridCacher.Set("key", "value"); 57 | var result = _remoteCacher.Get("key"); 58 | Assert.Equal("value", result); 59 | } 60 | 61 | [Fact] 62 | public void GetsFromLocalCacheFirst() 63 | { 64 | _remoteCacher.Set("key", "value1"); 65 | _localCacher.Set("key", "value2"); 66 | var result = _hybridCacher.Get("key"); 67 | Assert.Equal("value2", result); 68 | } 69 | 70 | [Fact] 71 | public void GetsFromRemoteCacheIfNotInLocal() 72 | { 73 | _remoteCacher.Set("key", "value1"); 74 | var result = _hybridCacher.Get("key"); 75 | Assert.Equal("value1", result); 76 | } 77 | 78 | [Fact] 79 | public void SetsExpirationOfLocalByRemoteTimeToLive() 80 | { 81 | _remoteCacher.Set("key", "value1", DateTime.Now.AddSeconds(120)); 82 | var hybridResult = _hybridCacher.Get("key"); 83 | var ttl = _localCacher.TimeToLive("key"); 84 | Assert.InRange(ttl, 118, 122); 85 | } 86 | 87 | [Fact] 88 | public void CanGetAllKeys() 89 | { 90 | _remoteCacher.Set("key1", "value"); 91 | _localCacher.Set("key2", "value"); 92 | var result = _hybridCacher.GetAllKeys(); 93 | Assert.True(result.Select(k => k.KeyName).Contains(_cacheSleeve.AddPrefix("key1"))); 94 | Assert.True(result.Select(k => k.KeyName).Contains(_cacheSleeve.AddPrefix("key2"))); 95 | } 96 | 97 | [Fact] 98 | public void ExpirationTransfersFromRemoteToLocal() 99 | { 100 | _remoteCacher.Set("key1", "value", DateTime.Now.AddSeconds(120)); 101 | _hybridCacher.Get("key1"); 102 | var results = _localCacher.GetAllKeys(); 103 | Assert.InRange(results.First().ExpirationDate.Value, DateTime.Now.AddSeconds(118), DateTime.Now.AddSeconds(122)); 104 | } 105 | } 106 | 107 | public class PubSub : HybridCacherTests 108 | { 109 | [Fact] 110 | public void SetCausesPublishRemove() 111 | { 112 | var lastMessage = default(string); 113 | SubscriptionHit += (key, message) => { lastMessage = message; }; 114 | _hybridCacher.Set("key", "value"); 115 | Thread.Sleep(30); 116 | Assert.Equal("key", lastMessage); 117 | } 118 | 119 | [Fact] 120 | public void RemoveCausesPublishRemove() 121 | { 122 | var lastMessage = default(string); 123 | SubscriptionHit += (key, message) => { lastMessage = message; }; 124 | _hybridCacher.Remove("key"); 125 | Thread.Sleep(30); 126 | Assert.Equal("key", lastMessage); 127 | } 128 | 129 | [Fact] 130 | public void FlushCausesPublishFlush() 131 | { 132 | var lastMessage = default(string); 133 | SubscriptionHit += (key, message) => { lastMessage = message; }; 134 | _hybridCacher.FlushAll(); 135 | Thread.Sleep(100); 136 | Assert.Equal("flush", lastMessage); 137 | } 138 | } 139 | 140 | public class Dependencies : HybridCacherTests 141 | { 142 | [Fact] 143 | public void GetSetsRemoteDependencyOnLocal() 144 | { 145 | _hybridCacher.Set("key1", "value1"); 146 | _hybridCacher.Get("key1"); 147 | _hybridCacher.Set("key2", "value2", "key1"); 148 | _hybridCacher.Get("key2"); 149 | var result = _localCacher.Get("key2"); 150 | Assert.Equal("value2", result); 151 | _localCacher.Remove("key1"); 152 | result = _localCacher.Get("key2"); 153 | Assert.Equal(null, result); 154 | } 155 | } 156 | 157 | public void Dispose() 158 | { 159 | _hybridCacher.FlushAll(); 160 | _hybridCacher = null; 161 | _remoteCacher = null; 162 | _localCacher = null; 163 | } 164 | 165 | /// 166 | /// Converts a byte[] to a string. 167 | /// 168 | /// The bytes to convert. 169 | /// The resulting string. 170 | private static string GetString(byte[] bytes) 171 | { 172 | var buffer = Encoding.Convert(Encoding.GetEncoding("iso-8859-1"), Encoding.UTF8, bytes); 173 | return Encoding.UTF8.GetString(buffer, 0, bytes.Count()); 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) 32 | 33 | 34 | 35 | 36 | $(SolutionDir).nuget 37 | packages.config 38 | 39 | 40 | 41 | 42 | $(NuGetToolsPath)\NuGet.exe 43 | @(PackageSource) 44 | 45 | "$(NuGetExePath)" 46 | mono --runtime=v4.0.30319 $(NuGetExePath) 47 | 48 | $(TargetDir.Trim('\\')) 49 | 50 | -RequireConsent 51 | -NonInteractive 52 | 53 | "$(SolutionDir) " 54 | "$(SolutionDir)" 55 | 56 | 57 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) 58 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols 59 | 60 | 61 | 62 | RestorePackages; 63 | $(BuildDependsOn); 64 | 65 | 66 | 67 | 68 | $(BuildDependsOn); 69 | BuildPackage; 70 | 71 | 72 | 73 | 74 | 75 | 76 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 91 | 92 | 95 | 96 | 97 | 98 | 100 | 101 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /CacheSleeve.Tests/HybridCacherAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Web; 6 | using StackExchange.Redis; 7 | using Xunit; 8 | 9 | namespace CacheSleeve.Tests 10 | { 11 | public class HybridCacherAsyncTests : IDisposable 12 | { 13 | private HybridCacher _hybridCacher; 14 | private RedisCacher _remoteCacher; 15 | private HttpContextCacher _localCacher; 16 | private readonly CacheManager _cacheSleeve; 17 | 18 | private delegate void SubscriptionHitHandler(string key, string message); 19 | private event SubscriptionHitHandler SubscriptionHit; 20 | private void OnSubscriptionHit(string key, string message) 21 | { 22 | if (SubscriptionHit != null) 23 | SubscriptionHit(key, message); 24 | } 25 | 26 | public HybridCacherAsyncTests() 27 | { 28 | // have to fake an http context to use http context cache 29 | HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null)); 30 | 31 | CacheManager.Init(TestSettings.RedisHost, TestSettings.RedisPort, TestSettings.RedisPassword, TestSettings.RedisDb, TestSettings.KeyPrefix); 32 | _cacheSleeve = CacheManager.Settings; 33 | 34 | var cacheSleeve = CacheManager.Settings; 35 | 36 | var configuration = 37 | ConfigurationOptions.Parse(string.Format("{0}:{1}", TestSettings.RedisHost, TestSettings.RedisPort)); 38 | configuration.AllowAdmin = true; 39 | var redisConnection = ConnectionMultiplexer.Connect(configuration); 40 | 41 | var subscriber = redisConnection.GetSubscriber(); 42 | subscriber.Subscribe("cacheSleeve.remove.*", (redisChannel, value) => OnSubscriptionHit(redisChannel, GetString(value))); 43 | subscriber.Subscribe("cacheSleeve.flush*", (redisChannel, value) => OnSubscriptionHit(redisChannel, "flush")); 44 | 45 | _hybridCacher = new HybridCacher(); 46 | _remoteCacher = cacheSleeve.RemoteCacher; 47 | _localCacher = cacheSleeve.LocalCacher; 48 | } 49 | 50 | 51 | public class Basics : HybridCacherAsyncTests 52 | { 53 | [Fact] 54 | public async void SetCachesRemote() 55 | { 56 | await _hybridCacher.SetAsync("key", "value"); 57 | var result = await _remoteCacher.GetAsync("key"); 58 | Assert.Equal("value", result); 59 | } 60 | 61 | [Fact] 62 | public async void GetsFromLocalCacheFirst() 63 | { 64 | await _remoteCacher.SetAsync("key", "value1"); 65 | _localCacher.Set("key", "value2"); 66 | var result = await _hybridCacher.GetAsync("key"); 67 | Assert.Equal("value2", result); 68 | } 69 | 70 | [Fact] 71 | public async void GetsFromRemoteCacheIfNotInLocal() 72 | { 73 | await _remoteCacher.SetAsync("key", "value1"); 74 | var result = await _hybridCacher.GetAsync("key"); 75 | Assert.Equal("value1", result); 76 | } 77 | 78 | [Fact] 79 | public async void SetsExpirationOfLocalByRemoteTimeToLive() 80 | { 81 | await _remoteCacher.SetAsync("key", "value1", DateTime.Now.AddSeconds(120)); 82 | var hybridResult = await _hybridCacher.GetAsync("key"); 83 | var ttl = _localCacher.TimeToLive("key"); 84 | Assert.InRange(ttl, 118, 122); 85 | } 86 | 87 | [Fact] 88 | public async void CanGetAllKeys() 89 | { 90 | await _remoteCacher.SetAsync("key1", "value"); 91 | _localCacher.Set("key2", "value"); 92 | var result = await _hybridCacher.GetAllKeysAsync(); 93 | Assert.True(result.Select(k => k.KeyName).Contains(_cacheSleeve.AddPrefix("key1"))); 94 | Assert.True(result.Select(k => k.KeyName).Contains(_cacheSleeve.AddPrefix("key2"))); 95 | } 96 | 97 | [Fact] 98 | public async void ExpirationTransfersFromRemoteToLocal() 99 | { 100 | await _remoteCacher.SetAsync("key1", "value", DateTime.Now.AddSeconds(120)); 101 | await _hybridCacher.GetAsync("key1"); 102 | var results = _localCacher.GetAllKeys(); 103 | Assert.InRange(results.First().ExpirationDate.Value, DateTime.Now.AddSeconds(118), DateTime.Now.AddSeconds(122)); 104 | } 105 | } 106 | 107 | public class PubSub : HybridCacherAsyncTests 108 | { 109 | [Fact] 110 | public async void SetCausesPublishRemove() 111 | { 112 | var lastMessage = default(string); 113 | SubscriptionHit += (key, message) => { lastMessage = message; }; 114 | await _hybridCacher.SetAsync("key", "value"); 115 | Thread.Sleep(100); 116 | Assert.Equal("key", lastMessage); 117 | } 118 | 119 | [Fact] 120 | public async void RemoveCausesPublishRemove() 121 | { 122 | var lastMessage = default(string); 123 | SubscriptionHit += (key, message) => { lastMessage = message; }; 124 | await _hybridCacher.RemoveAsync("key"); 125 | Thread.Sleep(30); 126 | Assert.Equal("key", lastMessage); 127 | } 128 | 129 | [Fact] 130 | public async void FlushCausesPublishFlush() 131 | { 132 | var lastMessage = default(string); 133 | SubscriptionHit += (key, message) => { lastMessage = message; }; 134 | await _hybridCacher.FlushAllAsync(); 135 | Thread.Sleep(30); 136 | Assert.Equal("flush", lastMessage); 137 | } 138 | } 139 | 140 | public class Dependencies : HybridCacherAsyncTests 141 | { 142 | [Fact] 143 | public async void GetSetsRemoteDependencyOnLocal() 144 | { 145 | await _hybridCacher.SetAsync("key1", "value1"); 146 | await _hybridCacher.GetAsync("key1"); 147 | await _hybridCacher.SetAsync("key2", "value2", "key1"); 148 | await _hybridCacher.GetAsync("key2"); 149 | var result = _localCacher.Get("key2"); 150 | Assert.Equal("value2", result); 151 | _localCacher.Remove("key1"); 152 | result = _localCacher.Get("key2"); 153 | Assert.Equal(null, result); 154 | } 155 | } 156 | 157 | public void Dispose() 158 | { 159 | _hybridCacher.FlushAll(); 160 | _hybridCacher = null; 161 | _remoteCacher = null; 162 | _localCacher = null; 163 | } 164 | 165 | /// 166 | /// Converts a byte[] to a string. 167 | /// 168 | /// The bytes to convert. 169 | /// The resulting string. 170 | private static string GetString(byte[] bytes) 171 | { 172 | var buffer = Encoding.Convert(Encoding.GetEncoding("iso-8859-1"), Encoding.UTF8, bytes); 173 | return Encoding.UTF8.GetString(buffer, 0, bytes.Count()); 174 | } 175 | } 176 | } -------------------------------------------------------------------------------- /CacheSleeve.NET40/RedisCacher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using CacheSleeve.Models; 7 | using Newtonsoft.Json; 8 | using StackExchange.Redis; 9 | 10 | namespace CacheSleeve 11 | { 12 | public partial class RedisCacher : ICacher 13 | { 14 | private readonly ICacheManager _cacheSleeve; 15 | private readonly IObjectSerializer _objectSerializer; 16 | 17 | public RedisCacher() 18 | { 19 | _cacheSleeve = CacheManager.Settings; 20 | _objectSerializer = new JsonObjectSerializer(); 21 | } 22 | 23 | public RedisCacher( 24 | ICacheManager cacheManger, 25 | IObjectSerializer serializer) 26 | { 27 | _cacheSleeve = cacheManger; 28 | _objectSerializer = serializer; 29 | } 30 | 31 | 32 | public T Get(string key) 33 | { 34 | var conn = _cacheSleeve.GetDatebase(); 35 | var redisKey = _cacheSleeve.AddPrefix(key); 36 | if (typeof(T) == typeof(string) || typeof(T) == typeof(byte[])) 37 | return (T)(dynamic)conn.StringGet(redisKey); 38 | string result; 39 | try 40 | { 41 | result = conn.StringGet(redisKey); 42 | } 43 | catch (Exception) 44 | { 45 | return default(T); 46 | } 47 | if (result != null) 48 | return _objectSerializer.DeserializeObject(result); 49 | return default(T); 50 | } 51 | 52 | public bool Set(string key, T value, string parentKey = null) 53 | { 54 | var redisKey = _cacheSleeve.AddPrefix(key); 55 | if (InternalSet(redisKey, value)) 56 | { 57 | RemoveDependencies(redisKey); 58 | SetDependencies(redisKey, _cacheSleeve.AddPrefix(parentKey)); 59 | } 60 | return true; 61 | } 62 | 63 | public bool Set(string key, T value, DateTime expiresAt, string parentKey = null) 64 | { 65 | return Set(key, value, expiresAt - DateTime.Now, parentKey); 66 | } 67 | 68 | public bool Set(string key, T value, TimeSpan expiresIn, string parentKey = null) 69 | { 70 | 71 | var redisKey = _cacheSleeve.AddPrefix(key); 72 | var result = InternalSet(redisKey, value); 73 | if (result) 74 | { 75 | var conn = _cacheSleeve.GetDatebase(); 76 | result = conn.KeyExpire(redisKey, expiresIn); 77 | RemoveDependencies(redisKey); 78 | SetDependencies(redisKey, _cacheSleeve.AddPrefix(parentKey)); 79 | } 80 | return result; 81 | } 82 | 83 | public bool Remove(string key) 84 | { 85 | var conn = _cacheSleeve.GetDatebase(); 86 | var redisKey = _cacheSleeve.AddPrefix(key); 87 | if (conn.KeyDelete(redisKey)) 88 | { 89 | RemoveDependencies(redisKey); 90 | conn.KeyDelete(redisKey + ".parent"); 91 | if (_cacheSleeve.Debug) 92 | Trace.WriteLine(string.Format("CS Redis: Removed cache item with key {0}", key)); 93 | return true; 94 | } 95 | return false; 96 | } 97 | 98 | public void FlushAll() 99 | { 100 | var keys = _cacheSleeve.GetAllKeys(); 101 | _cacheSleeve.GetDatebase().KeyDelete(keys.ToArray()); 102 | } 103 | 104 | public IEnumerable GetAllKeys() 105 | { 106 | var conn = _cacheSleeve.GetDatebase(); 107 | var keys = new List(); 108 | var keyStrings = _cacheSleeve.GetAllKeys(); 109 | foreach (var keyString in keyStrings) 110 | { 111 | var ttl = conn.KeyTimeToLive(keyString); 112 | var expiration = default(DateTime?); 113 | if (ttl != null) 114 | expiration = DateTime.Now.AddSeconds(ttl.Value.TotalSeconds); 115 | keys.Add(new Key(keyString, expiration)); 116 | } 117 | return keys; 118 | } 119 | 120 | /// 121 | /// Gets the amount of time left before the item expires. 122 | /// 123 | /// The key to check. 124 | /// The amount of time in seconds. 125 | public long TimeToLive(string key) 126 | { 127 | var conn = _cacheSleeve.GetDatebase(); 128 | var ttl = conn.KeyTimeToLive(_cacheSleeve.AddPrefix(key)); 129 | if (ttl == null) 130 | return -1; 131 | return (long)ttl.Value.TotalSeconds; 132 | } 133 | 134 | /// 135 | /// Publishes a message with a specified key. 136 | /// Any clients connected to the Redis server and subscribed to the key will recieve the message. 137 | /// 138 | /// The key that other clients subscribe to. 139 | /// The message to send to subscribed clients. 140 | public void PublishToKey(string key, string message) 141 | { 142 | var conn = _cacheSleeve.GetDatebase(); 143 | conn.Publish(key, message); 144 | } 145 | 146 | 147 | /// 148 | /// Shared insert for public wrappers. 149 | /// 150 | /// The type of the item to insert. 151 | /// The key of the item to insert. 152 | /// The value of the item to insert. 153 | /// 154 | private bool InternalSet(string key, T value) 155 | { 156 | var conn = _cacheSleeve.GetDatebase(); 157 | try 158 | { 159 | if (typeof(T) == typeof(byte[])) 160 | { 161 | conn.StringSet(key, value as byte[]); 162 | } 163 | else if (typeof(T) == typeof(string)) 164 | { 165 | conn.StringSet(key, value as string); 166 | } 167 | else 168 | { 169 | var serializedValue = _objectSerializer.SerializeObject(value); 170 | conn.StringSet(key, serializedValue); 171 | } 172 | if (_cacheSleeve.Debug) 173 | Trace.WriteLine(string.Format("CS Redis: Set cache item with key {0}", key)); 174 | } 175 | catch (Exception) 176 | { 177 | return false; 178 | } 179 | return true; 180 | } 181 | 182 | /// 183 | /// Adds a child key as a dependency of a parent key. 184 | /// When the parent is invalidated by remove, overwrite, or expiration the child will be removed. 185 | /// 186 | /// The key of the child item. 187 | /// The key of the parent item. 188 | private void SetDependencies(string childKey, string parentKey) 189 | { 190 | if (childKey.Length <= _cacheSleeve.KeyPrefix.Length || parentKey.Length <= _cacheSleeve.KeyPrefix.Length) 191 | return; 192 | 193 | var conn = _cacheSleeve.GetDatebase(); 194 | var parentDepKey = parentKey + ".children"; 195 | var childDepKey = childKey + ".parent"; 196 | conn.ListRightPush(parentDepKey, childKey); 197 | conn.StringSet(childDepKey, parentKey); 198 | var ttl = conn.KeyTimeToLive(parentKey); 199 | if (ttl != null && ttl.Value.TotalSeconds > -1) 200 | { 201 | var children = conn.ListRange(parentDepKey, 0, -1).ToList(); 202 | conn.KeyExpire(parentDepKey, ttl); 203 | conn.KeyExpire(childDepKey, ttl); 204 | foreach (var child in children) 205 | conn.KeyExpire(child.ToString(), ttl); 206 | } 207 | } 208 | 209 | /// 210 | /// Removes all of the dependencies of the key from the cache. 211 | /// 212 | /// The key of the item to remove children for. 213 | private void RemoveDependencies(string key) 214 | { 215 | if (key.Length <= _cacheSleeve.KeyPrefix.Length) 216 | return; 217 | 218 | var conn = _cacheSleeve.GetDatebase(); 219 | var depKey = key + ".children"; 220 | var children = conn.ListRange(depKey, 0, -1).ToList(); 221 | if (children.Count > 0) 222 | { 223 | var keys = new List(children.Count * 2 + 1); 224 | keys.Add(depKey); 225 | foreach (var child in children) 226 | { 227 | keys.Add(child.ToString()); 228 | keys.Add(child + ".parent"); 229 | } 230 | conn.KeyDelete(keys.ToArray()); 231 | } 232 | } 233 | } 234 | } -------------------------------------------------------------------------------- /CacheSleeve/RedisCacherAsync.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using CacheSleeve.Models; 7 | using Newtonsoft.Json; 8 | using StackExchange.Redis; 9 | 10 | namespace CacheSleeve 11 | { 12 | public partial class RedisCacher : IAsyncCacher 13 | { 14 | public async Task GetAsync(string key) 15 | { 16 | var conn = _cacheSleeve.GetDatebase(); 17 | 18 | var redisKey = _cacheSleeve.AddPrefix(key); 19 | if (typeof(T) == typeof(string) || typeof(T) == typeof(byte[])) 20 | return (T)(dynamic)(await conn.StringGetAsync(redisKey)); 21 | string result; 22 | try 23 | { 24 | result = await conn.StringGetAsync(_cacheSleeve.AddPrefix(key)); 25 | } 26 | catch (Exception) 27 | { 28 | return default(T); 29 | } 30 | if (result != null) 31 | return _objectSerializer.DeserializeObject(result); 32 | return default(T); 33 | } 34 | 35 | public async Task SetAsync(string key, T value, string parentKey = null) 36 | { 37 | var redisKey = _cacheSleeve.AddPrefix(key); 38 | if (await InternalSetAsync(redisKey, value)) 39 | { 40 | await RemoveDependenciesAsync(redisKey); 41 | await SetDependenciesAsync(redisKey, _cacheSleeve.AddPrefix(parentKey)); 42 | } 43 | return true; 44 | } 45 | 46 | public Task SetAsync(string key, T value, DateTime expiresAt, string parentKey = null) 47 | { 48 | return SetAsync(key, value, expiresAt - DateTime.Now, parentKey); 49 | } 50 | 51 | public async Task SetAsync(string key, T value, TimeSpan expiresIn, string parentKey = null) 52 | { 53 | var redisKey = _cacheSleeve.AddPrefix(key); 54 | var result = await InternalSetAsync(redisKey, value); 55 | if (result) 56 | { 57 | var conn = _cacheSleeve.GetDatebase(); 58 | result = await conn.KeyExpireAsync(redisKey, expiresIn); 59 | await RemoveDependenciesAsync(redisKey); 60 | await SetDependenciesAsync(redisKey, _cacheSleeve.AddPrefix(parentKey)); 61 | } 62 | return result; 63 | } 64 | 65 | public async Task RemoveAsync(string key) 66 | { 67 | var conn = _cacheSleeve.GetDatebase(); 68 | var redisKey = _cacheSleeve.AddPrefix(key); 69 | if (await conn.KeyDeleteAsync(redisKey)) 70 | { 71 | await RemoveDependenciesAsync(redisKey); 72 | await conn.KeyDeleteAsync(redisKey + ".parent"); 73 | if (_cacheSleeve.Debug) 74 | Trace.WriteLine(string.Format("CS Redis: Removed cache item with key {0}", key)); 75 | return true; 76 | } 77 | return false; 78 | } 79 | 80 | public async Task FlushAllAsync() 81 | { 82 | var keys = _cacheSleeve.GetAllKeys(); 83 | await _cacheSleeve.GetDatebase().KeyDeleteAsync(keys.ToString()); 84 | } 85 | 86 | public async Task> GetAllKeysAsync() 87 | { 88 | var conn = _cacheSleeve.GetDatebase(); 89 | var keys = new List(); 90 | var keyStrings = _cacheSleeve.GetAllKeys().ToList(); 91 | var tasks = new Dictionary>(); 92 | foreach (var keyString in keyStrings) 93 | tasks.Add(keyString, conn.KeyTimeToLiveAsync(keyString)); 94 | await Task.WhenAll(tasks.Values.ToArray()); 95 | foreach (var taskResult in tasks) 96 | { 97 | var key = taskResult.Key; 98 | var ttl = taskResult.Value.Result; 99 | keys.Add(new Key(key, ttl != null ? DateTime.Now.AddSeconds(ttl.Value.TotalSeconds) : (DateTime?)null)); 100 | } 101 | return keys; 102 | } 103 | 104 | /// 105 | /// Gets the amount of time left before the item expires. 106 | /// 107 | /// The key to check. 108 | /// The amount of time in seconds. 109 | public async Task TimeToLiveAsync(string key) 110 | { 111 | var conn = _cacheSleeve.GetDatebase(); 112 | var ttl = await conn.KeyTimeToLiveAsync(_cacheSleeve.AddPrefix(key)); 113 | if (ttl == null) 114 | return -1; 115 | return (long)ttl.Value.TotalSeconds; 116 | } 117 | 118 | /// 119 | /// Publishes a message with a specified key. 120 | /// Any clients connected to the Redis server and subscribed to the key will recieve the message. 121 | /// 122 | /// The key that other clients subscribe to. 123 | /// The message to send to subscribed clients. 124 | public async Task PublishToKeyAsync(string key, string message) 125 | { 126 | var conn = _cacheSleeve.GetDatebase(); 127 | await conn.PublishAsync(key, message); 128 | } 129 | 130 | 131 | /// 132 | /// Shared insert for public wrappers. 133 | /// 134 | /// The type of the item to insert. 135 | /// The key of the item to insert. 136 | /// The value of the item to insert. 137 | /// 138 | private async Task InternalSetAsync(string key, T value) 139 | { 140 | var conn = _cacheSleeve.GetDatebase(); 141 | try 142 | { 143 | if (typeof(T) == typeof(byte[])) 144 | { 145 | await conn.StringSetAsync(key, value as byte[]); 146 | } 147 | else if (typeof(T) == typeof(string)) 148 | { 149 | await conn.StringSetAsync(key, value as string); 150 | } 151 | else 152 | { 153 | var serializedValue = _objectSerializer.SerializeObject(value); 154 | await conn.StringSetAsync(key, serializedValue); 155 | } 156 | if (_cacheSleeve.Debug) 157 | Trace.WriteLine(string.Format("CS Redis: Set cache item with key {0}", key)); 158 | } 159 | catch (Exception) 160 | { 161 | return false; 162 | } 163 | return true; 164 | } 165 | 166 | /// 167 | /// Adds a child key as a dependency of a parent key. 168 | /// When the parent is invalidated by remove, overwrite, or expiration the child will be removed. 169 | /// 170 | /// The key of the child item. 171 | /// The key of the parent item. 172 | private async Task SetDependenciesAsync(string childKey, string parentKey) 173 | { 174 | if (childKey.Length <= _cacheSleeve.KeyPrefix.Length || parentKey.Length <= _cacheSleeve.KeyPrefix.Length) 175 | return; 176 | 177 | var conn = _cacheSleeve.GetDatebase(); 178 | var parentDepKey = parentKey + ".children"; 179 | var childDepKey = childKey + ".parent"; 180 | var parentKetPushTask = conn.ListRightPushAsync(parentDepKey, childKey); 181 | var childKeySetTask = conn.StringSetAsync(childDepKey, parentKey); 182 | var ttlTask = conn.KeyTimeToLiveAsync(parentKey); 183 | await Task.WhenAll(parentKetPushTask, childKeySetTask, ttlTask); 184 | var ttl = ttlTask.Result; 185 | if (ttl != null && ttl.Value.TotalSeconds > -1) 186 | { 187 | var children = (await conn.ListRangeAsync(parentDepKey, 0, -1)).ToList(); 188 | var expirationTasks = new List(children.Count + 2); 189 | expirationTasks.Add(conn.KeyExpireAsync(parentDepKey, ttl)); 190 | expirationTasks.Add(conn.KeyExpireAsync(childDepKey, ttl)); 191 | foreach (var child in children) 192 | expirationTasks.Add(conn.KeyExpireAsync(child.ToString(), ttl)); 193 | await Task.WhenAll(expirationTasks.ToArray()); 194 | } 195 | } 196 | 197 | /// 198 | /// Removes all of the dependencies of the key from the cache. 199 | /// 200 | /// The key of the item to remove children for. 201 | private async Task RemoveDependenciesAsync(string key) 202 | { 203 | if (key.Length <= _cacheSleeve.KeyPrefix.Length) 204 | return; 205 | 206 | var conn = _cacheSleeve.GetDatebase(); 207 | var depKey = key + ".children"; 208 | var children = (await conn.ListRangeAsync(depKey, 0, -1)).ToList(); 209 | if (children.Count > 0) 210 | { 211 | var keys = new List(children.Count * 2 + 1); 212 | keys.Add(depKey); 213 | foreach (var child in children) 214 | { 215 | keys.Add(child.ToString()); 216 | keys.Add(child + ".parent"); 217 | } 218 | await conn.KeyDeleteAsync(keys.ToArray()); 219 | } 220 | } 221 | } 222 | } -------------------------------------------------------------------------------- /CacheSleeve.Tests/HttpContextCacherTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Web; 6 | using CacheSleeve.Tests.TestObjects; 7 | using Xunit; 8 | 9 | namespace CacheSleeve.Tests 10 | { 11 | public class HttpContextCacherTests : IDisposable 12 | { 13 | private HttpContextCacher _httpContextCacher; 14 | private readonly CacheManager _cacheSleeve; 15 | 16 | public HttpContextCacherTests() 17 | { 18 | // have to fake an http context to use http context cache 19 | HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null)); 20 | 21 | CacheManager.Init(TestSettings.RedisHost, TestSettings.RedisPort, TestSettings.RedisPassword, TestSettings.RedisDb, TestSettings.KeyPrefix); 22 | _cacheSleeve = CacheManager.Settings; 23 | 24 | _httpContextCacher = CacheManager.Settings.LocalCacher; 25 | } 26 | 27 | public class Basics : HttpContextCacherTests 28 | { 29 | [Fact] 30 | public void SetReturnsTrueOnInsert() 31 | { 32 | var result = _httpContextCacher.Set("key", "value"); 33 | Assert.Equal(true, result); 34 | } 35 | 36 | [Fact] 37 | public void CanSetAndGetStringValues() 38 | { 39 | _httpContextCacher.Set("key", "value"); 40 | var result = _httpContextCacher.Get("key"); 41 | Assert.Equal("value", result); 42 | } 43 | 44 | [Fact] 45 | public void CanSetAndGetByteValues() 46 | { 47 | _httpContextCacher.Set("key", new byte[] { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }); 48 | var result = _httpContextCacher.Get("key"); 49 | Assert.Equal(new byte[] { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, result); 50 | } 51 | 52 | [Fact] 53 | public void CanSetAndGetObjectValues() 54 | { 55 | _httpContextCacher.Set("key", TestSettings.George); 56 | var result = _httpContextCacher.Get("key"); 57 | Assert.Equal(TestSettings.George.Name, result.Name); 58 | Assert.Equal(TestSettings.George.Bananas.First().Length, result.Bananas.First().Length); 59 | } 60 | 61 | [Fact] 62 | public void GetEmptyKeyReturnsNull() 63 | { 64 | var result = _httpContextCacher.Get("nonexistant"); 65 | Assert.Equal(null, result); 66 | } 67 | 68 | [Fact] 69 | public void SetExistingKeyOverwrites() 70 | { 71 | var george = TestSettings.George; 72 | var georgeJr = new Monkey("George Jr."); 73 | _httpContextCacher.Set("key", george); 74 | _httpContextCacher.Set("key", georgeJr); 75 | var result = _httpContextCacher.Get("key"); 76 | Assert.Equal(georgeJr.Name, result.Name); 77 | } 78 | 79 | [Fact] 80 | public void CanRemoveItem() 81 | { 82 | _httpContextCacher.Set("key", "value"); 83 | var result = _httpContextCacher.Get("key"); 84 | Assert.Equal("value", result); 85 | _httpContextCacher.Remove("key"); 86 | result = _httpContextCacher.Get("key"); 87 | Assert.Equal(null, result); 88 | } 89 | 90 | [Fact] 91 | public void CanGetAllKeys() 92 | { 93 | _httpContextCacher.Set("key1", "value"); 94 | _httpContextCacher.Set("key2", "value"); 95 | var result = _httpContextCacher.GetAllKeys(); 96 | Assert.True(result.Select(k => k.KeyName).Contains(_cacheSleeve.AddPrefix("key1"))); 97 | Assert.True(result.Select(k => k.KeyName).Contains(_cacheSleeve.AddPrefix("key2"))); 98 | } 99 | 100 | [Fact] 101 | public void GetAllKeysIncludesExpiration() 102 | { 103 | _httpContextCacher.Set("key1", "value", DateTime.Now.AddMinutes(1)); 104 | var result = _httpContextCacher.GetAllKeys(); 105 | Assert.InRange(result.ToList()[0].ExpirationDate.Value, DateTime.Now.AddSeconds(58), DateTime.Now.AddSeconds(62)); 106 | } 107 | } 108 | 109 | public class Failsafes : HttpContextCacherTests 110 | { 111 | // This test is removed as functionality is changed. Simple type comparision is not good enough as you may be asking for a base type 112 | // or interface. Correct comparisson would be to use IsAssignableFrom but this check adds too much overhead, idea is when you change types 113 | // to flush your cache(for in memory cache restarting app will flush it anyway). 114 | 115 | //[Fact] 116 | //public void RemovesAndReturnsDefaultIfGetItemNotOfValidTypeOfT() 117 | //{ 118 | // _httpContextCacher.Set("key", TestSettings.George); 119 | // var result = _httpContextCacher.Get("key"); 120 | // Assert.Equal(0, result); 121 | // var result2 = _httpContextCacher.Get("key"); 122 | // Assert.Equal(null, result2); 123 | //} 124 | 125 | [Fact] 126 | public void ThrowsExceptionIfGetItemNotOfValidTypeOfT() 127 | { 128 | _httpContextCacher.Set("key", TestSettings.George); 129 | Assert.Throws(typeof(InvalidCastException), () => _httpContextCacher.Get("key")); 130 | } 131 | } 132 | 133 | public class Expiration : HttpContextCacherTests 134 | { 135 | [Fact] 136 | public void SetsTimeToLiveByDateTime() 137 | { 138 | _httpContextCacher.Set("key", "value", DateTime.Now.AddMilliseconds(100)); 139 | var result = _httpContextCacher.Get("key"); 140 | Assert.Equal("value", result); 141 | Thread.Sleep(110); 142 | result = _httpContextCacher.Get("key"); 143 | Assert.Equal(null, result); 144 | } 145 | 146 | [Fact] 147 | public void SetsTimeToLiveByTimeSpan() 148 | { 149 | _httpContextCacher.Set("key", "value", new TimeSpan(0, 0, 0, 0, 100)); 150 | var result = _httpContextCacher.Get("key"); 151 | Assert.Equal("value", result); 152 | Thread.Sleep(110); 153 | result = _httpContextCacher.Get("key"); 154 | Assert.Equal(null, result); 155 | } 156 | 157 | [Fact] 158 | public void KeysHaveProperExpirationDates() 159 | { 160 | _httpContextCacher.Set("key", "value", DateTime.Now.AddMinutes(1)); 161 | var result = _httpContextCacher.GetAllKeys(); 162 | Assert.InRange(result.First().ExpirationDate.Value, DateTime.Now.AddSeconds(58), DateTime.Now.AddSeconds(62)); 163 | } 164 | } 165 | 166 | public class Dependencies : HttpContextCacherTests 167 | { 168 | [Fact] 169 | public void DeleteParentAlsoDeletesChildrenForSet() 170 | { 171 | _httpContextCacher.Set("parent", "value1"); 172 | _httpContextCacher.Set("child", "value2", "parent"); 173 | var result = _httpContextCacher.Get("child"); 174 | Assert.Equal("value2", result); 175 | _httpContextCacher.Remove("parent"); 176 | result = _httpContextCacher.Get("child"); 177 | Assert.Equal(null, result); 178 | } 179 | 180 | [Fact] 181 | public void DeleteParentAlsoDeletesChildrenForSetWithDateTime() 182 | { 183 | _httpContextCacher.Set("parent", "value1"); 184 | _httpContextCacher.Set("child", "value2", DateTime.Now.AddHours(1), "parent"); 185 | var result = _httpContextCacher.Get("child"); 186 | Assert.Equal("value2", result); 187 | _httpContextCacher.Remove("parent"); 188 | result = _httpContextCacher.Get("child"); 189 | Assert.Equal(null, result); 190 | } 191 | 192 | [Fact] 193 | public void DeleteParentAlsoDeletesChildrenForSetWithTimeSpan() 194 | { 195 | _httpContextCacher.Set("parent", "value1"); 196 | _httpContextCacher.Set("child", "value2", TimeSpan.FromHours(1), "parent"); 197 | var result = _httpContextCacher.Get("child"); 198 | Assert.Equal("value2", result); 199 | _httpContextCacher.Remove("parent"); 200 | result = _httpContextCacher.Get("child"); 201 | Assert.Equal(null, result); 202 | } 203 | 204 | [Fact] 205 | public void OverwritingParentRemovesChildren() 206 | { 207 | _httpContextCacher.Set("parent", "value1"); 208 | _httpContextCacher.Set("child", "value2", "parent"); 209 | var result = _httpContextCacher.Get("child"); 210 | Assert.Equal("value2", result); 211 | _httpContextCacher.Set("parent", "value3"); 212 | result = _httpContextCacher.Get("child"); 213 | Assert.Equal(null, result); 214 | } 215 | } 216 | 217 | public class Polymorphism : HttpContextCacherTests 218 | { 219 | [Fact] 220 | public void ProperlySerializesAndDeserializesPolymorphicTypes() 221 | { 222 | var fruits = new List 223 | { 224 | new Banana(4, "green") 225 | }; 226 | _httpContextCacher.Set("key", fruits); 227 | var result = _httpContextCacher.Get>("key"); 228 | Assert.IsType(result.First()); 229 | } 230 | } 231 | 232 | public void Dispose() 233 | { 234 | _httpContextCacher.FlushAll(); 235 | _httpContextCacher = null; 236 | HttpContext.Current = null; 237 | } 238 | } 239 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /CacheSleeve.Tests/RedisCacherTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using CacheSleeve.Tests.TestObjects; 6 | using Xunit; 7 | 8 | namespace CacheSleeve.Tests 9 | { 10 | public class RedisCacherTests : IDisposable 11 | { 12 | private RedisCacher _redisCacher; 13 | private readonly CacheManager _cacheSleeve; 14 | 15 | public RedisCacherTests() 16 | { 17 | // have to fake an http context to use http context cache 18 | HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null)); 19 | 20 | CacheManager.Init(TestSettings.RedisHost, TestSettings.RedisPort, TestSettings.RedisPassword, TestSettings.RedisDb, TestSettings.KeyPrefix); 21 | _cacheSleeve = CacheManager.Settings; 22 | 23 | _redisCacher = CacheManager.Settings.RemoteCacher; 24 | } 25 | 26 | public class Basics : RedisCacherTests 27 | { 28 | [Fact] 29 | public void SetReturnsTrueOnInsert() 30 | { 31 | var result = _redisCacher.Set("key", "value"); 32 | Assert.Equal(true, result); 33 | } 34 | 35 | [Fact] 36 | public void CanSetAndGetStringValues() 37 | { 38 | _redisCacher.Set("key", "value"); 39 | var result = _redisCacher.Get("key"); 40 | Assert.Equal("value", result); 41 | } 42 | 43 | [Fact] 44 | public void CanSetAndGetByteValues() 45 | { 46 | _redisCacher.Set("key", new byte[] { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }); 47 | var result = _redisCacher.Get("key"); 48 | Assert.Equal(new byte[] { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, result); 49 | } 50 | 51 | [Fact] 52 | public void CanSetAndGetObjectValues() 53 | { 54 | _redisCacher.Set("key", TestSettings.George); 55 | var result = _redisCacher.Get("key"); 56 | Assert.Equal(TestSettings.George.Name, result.Name); 57 | Assert.Equal(TestSettings.George.Bananas.First().Length, result.Bananas.First().Length); 58 | } 59 | 60 | [Fact] 61 | public void GetEmptyKeyReturnsNull() 62 | { 63 | var result = _redisCacher.Get("nonexistant"); 64 | Assert.Equal(null, result); 65 | } 66 | 67 | [Fact] 68 | public void SetExistingKeyOverwrites() 69 | { 70 | var george = TestSettings.George; 71 | var georgeJr = new Monkey("George Jr."); 72 | _redisCacher.Set("key", george); 73 | _redisCacher.Set("key", georgeJr); 74 | var result = _redisCacher.Get("key"); 75 | Assert.Equal(georgeJr.Name, result.Name); 76 | } 77 | 78 | [Fact] 79 | public void CanRemoveItem() 80 | { 81 | _redisCacher.Set("key", "value"); 82 | var result = _redisCacher.Get("key"); 83 | Assert.Equal("value", result); 84 | _redisCacher.Remove("key"); 85 | result = _redisCacher.Get("key"); 86 | Assert.Equal(null, result); 87 | } 88 | 89 | [Fact] 90 | public void CanGetAllKeys() 91 | { 92 | _redisCacher.Set("key1", "value"); 93 | _redisCacher.Set("key2", "value"); 94 | var result = _redisCacher.GetAllKeys(); 95 | Assert.True(result.Select(k => k.KeyName).Contains(_cacheSleeve.AddPrefix("key1"))); 96 | Assert.True(result.Select(k => k.KeyName).Contains(_cacheSleeve.AddPrefix("key2"))); 97 | } 98 | 99 | [Fact] 100 | public void GetAllKeysIncludesExpiration() 101 | { 102 | _redisCacher.Set("key1", "value", DateTime.Now.AddMinutes(1)); 103 | var result = _redisCacher.GetAllKeys(); 104 | Assert.InRange(result.ToList()[0].ExpirationDate.Value, DateTime.Now.AddSeconds(58), DateTime.Now.AddSeconds(62)); 105 | } 106 | 107 | [Fact] 108 | public void IfTimeToLiveIsNegative1ThenExpirationIsNull() 109 | { 110 | _redisCacher.Set("key1", "value"); 111 | var result = _redisCacher.GetAllKeys(); 112 | Assert.Equal(null, result.First().ExpirationDate); 113 | } 114 | } 115 | 116 | public class Failsafes : RedisCacherTests 117 | { 118 | // This test is removed as functionality is changed. Simple type comparision is not good enough as you may be asking for a base type 119 | // or interface. Correct comparisson would be to use IsAssignableFrom but this check adds too much overhead, idea is when you change types 120 | // to flush your cache(for in memory cache restarting app will flush it anyway). 121 | 122 | //[Fact] 123 | //public void RemovesAndReturnsDefaultIfGetItemNotOfValidTypeOfT() 124 | //{ 125 | // _redisCacher.Set("key", TestSettings.George); 126 | // var result = _redisCacher.Get("key"); 127 | // Assert.Equal(0, result); 128 | // var result2 = _redisCacher.Get("key"); 129 | // Assert.Equal(null, result2); 130 | //} 131 | 132 | [Fact] 133 | public void ThrowsExceptionIfGetItemNotOfValidTypeOfT() 134 | { 135 | _redisCacher.Set("key", TestSettings.George); 136 | Exception ex = null; 137 | try 138 | { 139 | _redisCacher.Get("key"); 140 | } 141 | catch (Exception e) 142 | { 143 | ex = e; 144 | } 145 | Assert.NotNull(ex); 146 | } 147 | } 148 | 149 | public class Expiration : RedisCacherTests 150 | { 151 | [Fact] 152 | public void SetsTimeToLiveByDateTime() 153 | { 154 | _redisCacher.Set("key", "value", DateTime.Now.AddMinutes(1)); 155 | var result = _redisCacher.TimeToLive("key"); 156 | Assert.InRange(result, 50, 70); 157 | } 158 | 159 | [Fact] 160 | public void SetsTimeToLiveByTimeSpan() 161 | { 162 | _redisCacher.Set("key", "value", new TimeSpan(0, 1, 0)); 163 | var result = _redisCacher.TimeToLive("key"); 164 | Assert.InRange(result, 50, 70); 165 | } 166 | 167 | [Fact] 168 | public void KeysHaveProperExpirationDates() 169 | { 170 | _redisCacher.Set("key", "value", DateTime.Now.AddMinutes(1)); 171 | var result = _redisCacher.GetAllKeys(); 172 | Assert.InRange(result.First().ExpirationDate.Value, DateTime.Now.AddSeconds(58), DateTime.Now.AddSeconds(62)); 173 | } 174 | } 175 | 176 | public class Dependencies : RedisCacherTests 177 | { 178 | [Fact] 179 | public void SetWithParentAddsKeyToParentsChildren() 180 | { 181 | _redisCacher.Set("key1", "value1"); 182 | _redisCacher.Set("key2", "value2", "key1"); 183 | var conn = _cacheSleeve.GetDatebase(); 184 | var childrenKey = CacheManager.Settings.AddPrefix("key1.children"); 185 | var result = conn.ListRange(childrenKey, 0, (int)conn.ListLength(childrenKey)); 186 | Assert.Contains(TestSettings.KeyPrefix + "key2", result.Select(x => x.ToString())); 187 | } 188 | 189 | [Fact] 190 | public void SetWithParentAddsParentReferenceForChild() 191 | { 192 | _redisCacher.Set("key1", "value1"); 193 | _redisCacher.Set("key2", "value2", "key1"); 194 | var result = _redisCacher.Get("key2.parent"); 195 | Assert.Equal(CacheManager.Settings.AddPrefix("key1"), result); 196 | } 197 | 198 | [Fact] 199 | public void ParentsTimeToLiveAddedToChildrenList() 200 | { 201 | _redisCacher.Set("key1", "value1", DateTime.Now.AddHours(1)); 202 | _redisCacher.Set("key2", "value2", "key1"); 203 | var result = _redisCacher.TimeToLive("key1.children"); 204 | Assert.InRange(result, 3500, 3700); 205 | } 206 | 207 | [Fact] 208 | public void ParentsTimeToLiveAddedToChildren() 209 | { 210 | _redisCacher.Set("key1", "value1", DateTime.Now.AddHours(1)); 211 | _redisCacher.Set("key2", "value2", "key1"); 212 | var result = _redisCacher.TimeToLive("key2"); 213 | Assert.InRange(result, 3500, 3700); 214 | } 215 | 216 | [Fact] 217 | public void OverwritingItemRemovesChildren() 218 | { 219 | _redisCacher.Set("key1", "value1"); 220 | _redisCacher.Set("key2", "value2", "key1"); 221 | var result = _redisCacher.Get("key2"); 222 | Assert.Equal("value2", result); 223 | _redisCacher.Set("key1", "value3"); 224 | result = _redisCacher.Get("key2"); 225 | Assert.Equal(null, result); 226 | } 227 | 228 | [Fact] 229 | public void OverwritingItemRemovesChildList() 230 | { 231 | _redisCacher.Set("key1", "value1"); 232 | _redisCacher.Set("key2", "value2", "key1"); 233 | var conn = _cacheSleeve.GetDatebase(); 234 | var childrenKey = CacheManager.Settings.AddPrefix("key1.children"); 235 | var result = conn.ListRange(childrenKey, 0, (int)conn.ListLength(childrenKey)); 236 | Assert.Contains(TestSettings.KeyPrefix + "key2", result.Select(x => x.ToString())); 237 | _redisCacher.Set("key1", "value3"); 238 | result = conn.ListRange(childrenKey, 0, (int)conn.ListLength(childrenKey)); 239 | Assert.Equal(0, result.Length); 240 | } 241 | 242 | [Fact] 243 | public void RemovingItemRemovesChildren() 244 | { 245 | _redisCacher.Set("key1", "value1"); 246 | _redisCacher.Set("key2", "value2", "key1"); 247 | var result = _redisCacher.Get("key2"); 248 | Assert.Equal("value2", result); 249 | _redisCacher.Remove("key1"); 250 | result = _redisCacher.Get("key2"); 251 | Assert.Equal(null, result); 252 | } 253 | 254 | [Fact] 255 | public void RemovingItemRemovesChildList() 256 | { 257 | _redisCacher.Set("key1", "value1"); 258 | _redisCacher.Set("key2", "value2", "key1"); 259 | var conn = _cacheSleeve.GetDatebase(); 260 | var childrenKey = CacheManager.Settings.AddPrefix("key1.children"); 261 | var result = conn.ListRange(childrenKey, 0, (int)conn.ListLength(childrenKey)); 262 | Assert.Contains(TestSettings.KeyPrefix + "key2", result.Select(x => x.ToString())); 263 | _redisCacher.Remove("key1"); 264 | result = conn.ListRange(childrenKey, 0, (int)conn.ListLength(childrenKey)); 265 | Assert.Equal(0, result.Length); 266 | } 267 | 268 | [Fact] 269 | public void RemovingItemRemovesParentReference() 270 | { 271 | _redisCacher.Set("key1", "value1"); 272 | _redisCacher.Set("key2", "value2", "key1"); 273 | var result = _redisCacher.Get("key2.parent"); 274 | Assert.Equal(CacheManager.Settings.AddPrefix("key1"), result); 275 | _redisCacher.Remove("key2"); 276 | result = _redisCacher.Get("key2.parent"); 277 | Assert.Equal(null, result); 278 | } 279 | 280 | [Fact] 281 | public void SettingDependenciesDoesNotScrewUpTimeToLive() 282 | { 283 | _redisCacher.Set("parent1", "value1", DateTime.Now.AddMinutes(1)); 284 | var parentTtl = _redisCacher.TimeToLive("parent1"); 285 | Assert.InRange(parentTtl, 58, 62); 286 | _redisCacher.Set("key1", "value1", DateTime.Now.AddMinutes(10), "parent1"); 287 | var childTtl = _redisCacher.TimeToLive("key1"); 288 | parentTtl = _redisCacher.TimeToLive("parent1"); 289 | Assert.InRange(childTtl, 58, 62); // this is not a 10 minute range because when the parent expires so will the child 290 | Assert.InRange(parentTtl, 58, 62); 291 | } 292 | } 293 | 294 | public class Polymorphism : RedisCacherTests 295 | { 296 | [Fact] 297 | public void ProperlySerializesAndDeserializesPolymorphicTypes() 298 | { 299 | var fruits = new List 300 | { 301 | new Banana(4, "green") 302 | }; 303 | _redisCacher.Set("key", fruits); 304 | var result = _redisCacher.Get>("key"); 305 | Assert.IsType(result.First()); 306 | } 307 | } 308 | 309 | public void Dispose() 310 | { 311 | _redisCacher.FlushAll(); 312 | _redisCacher = null; 313 | } 314 | } 315 | } -------------------------------------------------------------------------------- /CacheSleeve.Tests/RedisCacherAsyncTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Web; 6 | using CacheSleeve.Tests.TestObjects; 7 | using Xunit; 8 | 9 | namespace CacheSleeve.Tests 10 | { 11 | public class RedisCacherAsyncTests : IDisposable 12 | { 13 | private RedisCacher _redisCacher; 14 | private readonly CacheManager _cacheSleeve; 15 | 16 | public RedisCacherAsyncTests() 17 | { 18 | // have to fake an http context to use http context cache 19 | HttpContext.Current = new HttpContext(new HttpRequest(null, "http://tempuri.org", null), new HttpResponse(null)); 20 | 21 | CacheManager.Init(TestSettings.RedisHost, TestSettings.RedisPort, TestSettings.RedisPassword, TestSettings.RedisDb, TestSettings.KeyPrefix); 22 | _cacheSleeve = CacheManager.Settings; 23 | 24 | _redisCacher = CacheManager.Settings.RemoteCacher; 25 | } 26 | 27 | public class Basics : RedisCacherAsyncTests 28 | { 29 | [Fact] 30 | public async void SetReturnsTrueOnInsert() 31 | { 32 | var result = await _redisCacher.SetAsync("key", "value"); 33 | Assert.Equal(true, result); 34 | } 35 | 36 | [Fact] 37 | public async void CanSetAndGetStringValues() 38 | { 39 | await _redisCacher.SetAsync("key", "value"); 40 | var result = await _redisCacher.GetAsync("key"); 41 | Assert.Equal("value", result); 42 | } 43 | 44 | [Fact] 45 | public async void CanSetAndGetByteValues() 46 | { 47 | await _redisCacher.SetAsync("key", new byte[] { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }); 48 | var result = await _redisCacher.GetAsync("key"); 49 | Assert.Equal(new byte[] { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, result); 50 | } 51 | 52 | [Fact] 53 | public async void CanSetAndGetObjectValues() 54 | { 55 | await _redisCacher.SetAsync("key", TestSettings.George); 56 | var result = await _redisCacher.GetAsync("key"); 57 | Assert.Equal(TestSettings.George.Name, result.Name); 58 | Assert.Equal(TestSettings.George.Bananas.First().Length, result.Bananas.First().Length); 59 | } 60 | 61 | [Fact] 62 | public async void GetEmptyKeyReturnsNull() 63 | { 64 | var result = await _redisCacher.GetAsync("nonexistant"); 65 | Assert.Equal(null, result); 66 | } 67 | 68 | [Fact] 69 | public async void SetExistingKeyOverwrites() 70 | { 71 | var george = TestSettings.George; 72 | var georgeJr = new Monkey("George Jr."); 73 | var task1 = _redisCacher.SetAsync("key", george); 74 | var task2 = _redisCacher.SetAsync("key", georgeJr); 75 | await Task.WhenAll(task1, task2); 76 | var result = await _redisCacher.GetAsync("key"); 77 | Assert.Equal(georgeJr.Name, result.Name); 78 | } 79 | 80 | [Fact] 81 | public async void CanRemoveItem() 82 | { 83 | await _redisCacher.SetAsync("key", "value"); 84 | var result = await _redisCacher.GetAsync("key"); 85 | Assert.Equal("value", result); 86 | await _redisCacher.RemoveAsync("key"); 87 | result = await _redisCacher.GetAsync("key"); 88 | Assert.Equal(null, result); 89 | } 90 | 91 | [Fact] 92 | public async void CanGetAllKeys() 93 | { 94 | await _redisCacher.SetAsync("key1", "value"); 95 | await _redisCacher.SetAsync("key2", "value"); 96 | var result = await _redisCacher.GetAllKeysAsync(); 97 | Assert.True(result.Select(k => k.KeyName).Contains(_cacheSleeve.AddPrefix("key1"))); 98 | Assert.True(result.Select(k => k.KeyName).Contains(_cacheSleeve.AddPrefix("key2"))); 99 | } 100 | 101 | [Fact] 102 | public async void GetAllKeysIncludesExpiration() 103 | { 104 | await _redisCacher.SetAsync("key1", "value", DateTime.Now.AddMinutes(1)); 105 | var result = await _redisCacher.GetAllKeysAsync(); 106 | Assert.InRange(result.ToList()[0].ExpirationDate.Value, DateTime.Now.AddSeconds(58), DateTime.Now.AddSeconds(62)); 107 | } 108 | 109 | [Fact] 110 | public async void IfTimeToLiveIsNegative1ThenExpirationIsNull() 111 | { 112 | await _redisCacher.SetAsync("key1", "value"); 113 | var result = await _redisCacher.GetAllKeysAsync(); 114 | Assert.Equal(null, result.First().ExpirationDate); 115 | } 116 | } 117 | 118 | public class Failsafes : RedisCacherAsyncTests 119 | { 120 | // This test is removed as functionality is changed. Simple type comparision is not good enough as you may be asking for a base type 121 | // or interface. Correct comparisson would be to use IsAssignableFrom but this check adds too much overhead, idea is when you change types 122 | // to flush your cache(for in memory cache restarting app will flush it anyway). 123 | 124 | //[Fact] 125 | //public async void RemovesAndReturnsDefaultIfGetItemNotOfValidTypeOfT() 126 | //{ 127 | // await _redisCacher.SetAsync("key", TestSettings.George); 128 | // var result = await _redisCacher.GetAsync("key"); 129 | // Assert.Equal(0, result); 130 | // var result2 = await _redisCacher.GetAsync("key"); 131 | // Assert.Equal(null, result2); 132 | //} 133 | 134 | [Fact] 135 | public async void ThrowsExceptionIfGetItemNotOfValidTypeOfT() 136 | { 137 | await _redisCacher.SetAsync("key", TestSettings.George); 138 | Exception ex = null; 139 | try 140 | { 141 | await _redisCacher.GetAsync("key"); 142 | } 143 | catch (Exception e) 144 | { 145 | ex = e; 146 | } 147 | Assert.NotNull(ex); 148 | } 149 | } 150 | 151 | public class Expiration : RedisCacherAsyncTests 152 | { 153 | [Fact] 154 | public async void SetsTimeToLiveByDateTime() 155 | { 156 | await _redisCacher.SetAsync("key", "value", DateTime.Now.AddMinutes(1)); 157 | var result = await _redisCacher.TimeToLiveAsync("key"); 158 | Assert.InRange(result, 50, 70); 159 | } 160 | 161 | [Fact] 162 | public async void SetsTimeToLiveByTimeSpan() 163 | { 164 | await _redisCacher.SetAsync("key", "value", new TimeSpan(0, 1, 0)); 165 | var result = await _redisCacher.TimeToLiveAsync("key"); 166 | Assert.InRange(result, 50, 70); 167 | } 168 | 169 | [Fact] 170 | public async void KeysHaveProperExpirationDates() 171 | { 172 | await _redisCacher.SetAsync("key", "value", DateTime.Now.AddMinutes(1)); 173 | var result = await _redisCacher.GetAllKeysAsync(); 174 | Assert.InRange(result.First().ExpirationDate.Value, DateTime.Now.AddSeconds(58), DateTime.Now.AddSeconds(62)); 175 | } 176 | } 177 | 178 | public class Dependencies : RedisCacherAsyncTests 179 | { 180 | [Fact] 181 | public async void SetWithParentAddsKeyToParentsChildren() 182 | { 183 | await _redisCacher.SetAsync("key1", "value1"); 184 | await _redisCacher.SetAsync("key2", "value2", "key1"); 185 | var conn = _cacheSleeve.GetDatebase(); 186 | var childrenKey = CacheManager.Settings.AddPrefix("key1.children"); 187 | var result = conn.ListRange(childrenKey, 0, (int)conn.ListLength(childrenKey)); 188 | Assert.Contains(TestSettings.KeyPrefix + "key2", result.Select(x => x.ToString())); 189 | } 190 | 191 | [Fact] 192 | public async void SetWithParentAddsParentReferenceForChild() 193 | { 194 | await _redisCacher.SetAsync("key1", "value1"); 195 | await _redisCacher.SetAsync("key2", "value2", "key1"); 196 | var result = await _redisCacher.GetAsync("key2.parent"); 197 | Assert.Equal(CacheManager.Settings.AddPrefix("key1"), result); 198 | } 199 | 200 | [Fact] 201 | public async void ParentsTimeToLiveAddedToChildrenList() 202 | { 203 | await _redisCacher.SetAsync("key1", "value1", DateTime.Now.AddHours(1)); 204 | await _redisCacher.SetAsync("key2", "value2", "key1"); 205 | var result = await _redisCacher.TimeToLiveAsync("key1.children"); 206 | Assert.InRange(result, 3500, 3700); 207 | } 208 | 209 | [Fact] 210 | public async void ParentsTimeToLiveAddedToChildren() 211 | { 212 | await _redisCacher.SetAsync("key1", "value1", DateTime.Now.AddHours(1)); 213 | await _redisCacher.SetAsync("key2", "value2", "key1"); 214 | var result = await _redisCacher.TimeToLiveAsync("key2"); 215 | Assert.InRange(result, 3500, 3700); 216 | } 217 | 218 | [Fact] 219 | public async void OverwritingItemRemovesChildren() 220 | { 221 | await _redisCacher.SetAsync("key1", "value1"); 222 | await _redisCacher.SetAsync("key2", "value2", "key1"); 223 | var result = await _redisCacher.GetAsync("key2"); 224 | Assert.Equal("value2", result); 225 | await _redisCacher.SetAsync("key1", "value3"); 226 | result = await _redisCacher.GetAsync("key2"); 227 | Assert.Equal(null, result); 228 | } 229 | 230 | [Fact] 231 | public async void OverwritingItemRemovesChildList() 232 | { 233 | await _redisCacher.SetAsync("key1", "value1"); 234 | await _redisCacher.SetAsync("key2", "value2", "key1"); 235 | var conn = _cacheSleeve.GetDatebase(); 236 | var childrenKey = CacheManager.Settings.AddPrefix("key1.children"); 237 | var result = conn.ListRange(childrenKey, 0, (int)conn.ListLength(childrenKey)); 238 | Assert.Contains(TestSettings.KeyPrefix + "key2", result.Select(x => x.ToString())); 239 | await _redisCacher.SetAsync("key1", "value3"); 240 | result = conn.ListRange(childrenKey, 0, (int)conn.ListLength(childrenKey)); 241 | Assert.Equal(0, result.Length); 242 | } 243 | 244 | [Fact] 245 | public async void RemovingItemRemovesChildren() 246 | { 247 | await _redisCacher.SetAsync("key1", "value1"); 248 | await _redisCacher.SetAsync("key2", "value2", "key1"); 249 | var result = _redisCacher.Get("key2"); 250 | Assert.Equal("value2", result); 251 | await _redisCacher.RemoveAsync("key1"); 252 | result = await _redisCacher.GetAsync("key2"); 253 | Assert.Equal(null, result); 254 | } 255 | 256 | [Fact] 257 | public async void RemovingItemRemovesChildList() 258 | { 259 | await _redisCacher.SetAsync("key1", "value1"); 260 | await _redisCacher.SetAsync("key2", "value2", "key1"); 261 | var conn = _cacheSleeve.GetDatebase(); 262 | var childrenKey = CacheManager.Settings.AddPrefix("key1.children"); 263 | var result = conn.ListRange(childrenKey, 0, (int)conn.ListLength(childrenKey)); 264 | Assert.Contains(TestSettings.KeyPrefix + "key2", result.Select(x => x.ToString())); 265 | await _redisCacher.RemoveAsync("key1"); 266 | result = conn.ListRange(childrenKey, 0, (int)conn.ListLength(childrenKey)); 267 | Assert.Equal(0, result.Length); 268 | } 269 | 270 | [Fact] 271 | public async void RemovingItemRemovesParentReference() 272 | { 273 | await _redisCacher.SetAsync("key1", "value1"); 274 | await _redisCacher.SetAsync("key2", "value2", "key1"); 275 | var result = await _redisCacher.GetAsync("key2.parent"); 276 | Assert.Equal(CacheManager.Settings.AddPrefix("key1"), result); 277 | await _redisCacher.RemoveAsync("key2"); 278 | result = await _redisCacher.GetAsync("key2.parent"); 279 | Assert.Equal(null, result); 280 | } 281 | 282 | [Fact] 283 | public async void SettingDependenciesDoesNotScrewUpTimeToLive() 284 | { 285 | await _redisCacher.SetAsync("parent1", "value1", DateTime.Now.AddMinutes(1)); 286 | var parentTtl = await _redisCacher.TimeToLiveAsync("parent1"); 287 | Assert.InRange(parentTtl, 58, 62); 288 | await _redisCacher.SetAsync("key1", "value1", DateTime.Now.AddMinutes(10), "parent1"); 289 | var childTtl = await _redisCacher.TimeToLiveAsync("key1"); 290 | parentTtl = await _redisCacher.TimeToLiveAsync("parent1"); 291 | Assert.InRange(childTtl, 58, 62); // this is not a 10 minute range because when the parent expires so will the child 292 | Assert.InRange(parentTtl, 58, 62); 293 | } 294 | } 295 | 296 | public class Polymorphism : RedisCacherAsyncTests 297 | { 298 | [Fact] 299 | public async void ProperlySerializesAndDeserializesPolymorphicTypes() 300 | { 301 | var fruits = new List 302 | { 303 | new Banana(4, "green") 304 | }; 305 | await _redisCacher.SetAsync("key", fruits); 306 | var result = await _redisCacher.GetAsync>("key"); 307 | Assert.IsType(result.First()); 308 | } 309 | } 310 | 311 | public void Dispose() 312 | { 313 | _redisCacher.FlushAll(); 314 | _redisCacher = null; 315 | } 316 | } 317 | } --------------------------------------------------------------------------------