├── .nuget ├── NuGet.exe ├── NuGet.Config └── NuGet.targets ├── CacheR.Model ├── CacheCommandType.cs ├── CacheEntry.cs ├── CacheCommand.cs ├── CacheEntryKeyComparer.cs ├── Properties │ └── AssemblyInfo.cs └── CacheR.Model.csproj ├── CacheR.Server ├── app.config ├── ICacheStore.cs ├── packages.config ├── Properties │ └── AssemblyInfo.cs ├── MemoryCacheStore.cs ├── Program.cs ├── CacheServer.cs └── CacheR.Server.csproj ├── CacheR.Client.Sample ├── app.config ├── Properties │ └── AssemblyInfo.cs ├── Program.cs └── CacheR.Client.Sample.csproj ├── CacheR.Client ├── packages.config ├── Properties │ └── AssemblyInfo.cs ├── CacheR.Client.csproj └── CacheClient.cs ├── .gitignore ├── README.md ├── Sakefile.shade └── CacheR.sln /.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidfowl/CacheR/HEAD/.nuget/NuGet.exe -------------------------------------------------------------------------------- /CacheR.Model/CacheCommandType.cs: -------------------------------------------------------------------------------- 1 | namespace CacheR.Model 2 | { 3 | public enum CacheCommandType 4 | { 5 | Add, 6 | Remove 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CacheR.Server/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /CacheR.Client.Sample/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CacheR.Model/CacheEntry.cs: -------------------------------------------------------------------------------- 1 | namespace CacheR.Model 2 | { 3 | public class CacheEntry 4 | { 5 | public string Key { get; set; } 6 | public object Value { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CacheR.Model/CacheCommand.cs: -------------------------------------------------------------------------------- 1 | namespace CacheR.Model 2 | { 3 | public class CacheCommand 4 | { 5 | public CacheCommandType Type { get; set; } 6 | public CacheEntry[] Entries { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CacheR.Client/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | *.user 4 | *.suo 5 | *.cache 6 | *.docstates 7 | _ReSharper.* 8 | *.csproj.user 9 | *[Rr]e[Ss]harper.user 10 | _ReSharper.*/ 11 | packages/* 12 | target/* 13 | msbuild.log 14 | *.psess 15 | *.vsp 16 | *.pidb 17 | *.userprefs 18 | *DS_Store 19 | *.ncrunchsolution -------------------------------------------------------------------------------- /CacheR.Server/ICacheStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using CacheR.Model; 5 | 6 | namespace CacheR.Server 7 | { 8 | public interface ICacheStore 9 | { 10 | Task Save(CacheEntry entry); 11 | IEnumerable GetAll(); 12 | Task Delete(string key); 13 | 14 | Action OnEntryRemoved { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is this? 2 | My attempt at writing a scalable distributed cache on top of SignalR. 3 | 4 | ## How do I run it? 5 | 6 | 1. Run `CacheR.Server.exe` (by default it'll run on port 91). 7 | 2. Run 2 instances of the `CacheR.Client.Sample.exe`. 8 | 9 | The instructions in the client sample should be clear. You should be able to add a value in one client and have it be available in the other immediately 10 | 11 | ## Can I use this? 12 | This is far from production ready but if you're interesting in using it, let me know, I'd love the feedback. -------------------------------------------------------------------------------- /CacheR.Model/CacheEntryKeyComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace CacheR.Model 4 | { 5 | public class CacheEntryKeyComparer : IEqualityComparer 6 | { 7 | public readonly static CacheEntryKeyComparer Instance = new CacheEntryKeyComparer(); 8 | 9 | private CacheEntryKeyComparer() 10 | { 11 | } 12 | 13 | public bool Equals(CacheEntry x, CacheEntry y) 14 | { 15 | return x.Key.Equals(y.Key); 16 | } 17 | 18 | public int GetHashCode(CacheEntry obj) 19 | { 20 | return obj.Key.GetHashCode(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CacheR.Server/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Sakefile.shade: -------------------------------------------------------------------------------- 1 | use-standard-lifecycle 2 | use namespace="System" 3 | use namespace="System.IO" 4 | use import="Files" 5 | 6 | var PROJECT='CacheR' 7 | var VERSION='0.0.1' 8 | var FULL_VERSION='${VERSION}' 9 | var AUTHORS='${PROJECT} contributors' 10 | 11 | var BASE_DIR='${Directory.GetCurrentDirectory()}' 12 | var TARGET_DIR='${Path.Combine(BASE_DIR, "target")}' 13 | var BUILD_DIR='${Path.Combine(TARGET_DIR, "build")}' 14 | var TEST_DIR='${Path.Combine(TARGET_DIR, "test")}' 15 | var PACKAGE_DIR='${Path.Combine(TARGET_DIR, "package")}' 16 | 17 | var HOME_DIR='${Environment.GetEnvironmentVariable("HOME")}' 18 | set HOME_DIR='${Environment.GetEnvironmentVariable("HOMEDRIVE") + Environment.GetEnvironmentVariable("HOMEPATH")}' if='string.IsNullOrEmpty(HOME_DIR)' 19 | 20 | #clean-target-dir target='clean' 21 | directory delete='${TARGET_DIR}' 22 | 23 | #apply-version target='initialize' 24 | for each='var file in Files.Include("**/AssemblyInfo.cs")' 25 | assemblyinfo updateFile='${file}' assemblyVersion='${VERSION}' assemblyInformationalVersion='${FULL_VERSION}' 26 | 27 | #build-sln target='compile' 28 | build projectFile='${PROJECT}.sln' outputDir='${BUILD_DIR}' configuration='Release' 29 | 30 | -------------------------------------------------------------------------------- /CacheR.Client.Sample/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("CacheR")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Microsoft")] 12 | [assembly: AssemblyProduct("CacheR")] 13 | [assembly: AssemblyCopyright("Copyright © Microsoft 2012")] 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("74f9200f-8998-424c-9b35-eaca5373c972")] 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("0.0.1")] 35 | [assembly: AssemblyVersion("0.0.1")] 36 | [assembly: AssemblyFileVersion("0.0.1")] 37 | -------------------------------------------------------------------------------- /CacheR.Client/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("CacheR.Client")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Microsoft")] 12 | [assembly: AssemblyProduct("CacheR.Client")] 13 | [assembly: AssemblyCopyright("Copyright © Microsoft 2012")] 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("8e8980ed-2bf3-47f0-b4ee-62332f9d4ae8")] 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("0.0.1")] 35 | [assembly: AssemblyVersion("0.0.1")] 36 | [assembly: AssemblyFileVersion("0.0.1")] 37 | -------------------------------------------------------------------------------- /CacheR.Model/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("CacheR.Model")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Microsoft")] 12 | [assembly: AssemblyProduct("CacheR.Model")] 13 | [assembly: AssemblyCopyright("Copyright © Microsoft 2012")] 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("9841c0a4-827d-4351-a7f9-076fa909ae2e")] 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("0.0.1")] 35 | [assembly: AssemblyVersion("0.0.1")] 36 | [assembly: AssemblyFileVersion("0.0.1")] 37 | -------------------------------------------------------------------------------- /CacheR.Server/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("CacheR.Server")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Microsoft")] 12 | [assembly: AssemblyProduct("CacheR.Server")] 13 | [assembly: AssemblyCopyright("Copyright © Microsoft 2012")] 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("27f1d034-6a0e-4b21-80be-44c08cac77f6")] 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("0.0.1")] 35 | [assembly: AssemblyVersion("0.0.1")] 36 | [assembly: AssemblyFileVersion("0.0.1")] 37 | -------------------------------------------------------------------------------- /CacheR.Client.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CacheR.Client; 4 | 5 | namespace CacheR 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | var cache = new Cache("http://localhost:8087"); 12 | 13 | RunCacheTest(cache).Wait(); 14 | } 15 | 16 | private static async Task RunCacheTest(Cache cache) 17 | { 18 | await cache.ConnectAsync(); 19 | 20 | Console.WriteLine("Enter 'key=value' to add a value to the cache."); 21 | Console.WriteLine("Enter 'key' to get a value from the cache."); 22 | Console.WriteLine("Enter '-key' to delete a value from the cache."); 23 | 24 | string line = null; 25 | while ((line = Console.ReadLine()) != null) 26 | { 27 | var values = line.Split('='); 28 | if (values.Length == 2) 29 | { 30 | string key = values[0].Trim(); 31 | string value = values[1].Trim(); 32 | await cache.AddAsync(key, value); 33 | 34 | Console.WriteLine("Added '{0}' to the cache with value '{1}'.", key, value); 35 | } 36 | else if (line.StartsWith("-")) 37 | { 38 | string key = line.Substring(1).Trim(); 39 | await cache.DeleteAsync(key); 40 | 41 | Console.WriteLine("Deleting entry for key '{0}'", key); 42 | } 43 | else 44 | { 45 | string key = line.Trim(); 46 | Console.WriteLine("Value for '{0}' is " + cache.Get(key), key); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CacheR.Server/MemoryCacheStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Runtime.Caching; 5 | using System.Threading.Tasks; 6 | using CacheR.Model; 7 | 8 | namespace CacheR.Server 9 | { 10 | public class MemoryCacheStore : ICacheStore 11 | { 12 | private readonly MemoryCache _cache = MemoryCache.Default; 13 | private readonly Task _completedTask = CompletedTask(); 14 | 15 | public Action OnEntryRemoved { get; set; } 16 | 17 | public Task Save(CacheEntry entry) 18 | { 19 | var policy = new CacheItemPolicy(); 20 | 21 | // TODO: Allow this to be configured 22 | // Why 8 minutes? Because it feels right. 23 | policy.AbsoluteExpiration = DateTimeOffset.Now + TimeSpan.FromMinutes(8); 24 | policy.RemovedCallback = OnCacheEntryRemoved; 25 | _cache.Set(entry.Key, entry.Value, policy); 26 | 27 | return _completedTask; 28 | } 29 | 30 | public IEnumerable GetAll() 31 | { 32 | foreach (var entry in _cache) 33 | { 34 | yield return new CacheEntry 35 | { 36 | Key = entry.Key, 37 | Value = entry.Value 38 | }; 39 | } 40 | } 41 | 42 | public Task Delete(string key) 43 | { 44 | _cache.Remove(key); 45 | 46 | return _completedTask; 47 | } 48 | 49 | private static Task CompletedTask() 50 | { 51 | var tcs = new TaskCompletionSource(); 52 | tcs.SetResult(null); 53 | return tcs.Task; 54 | } 55 | 56 | public void OnCacheEntryRemoved(CacheEntryRemovedArguments args) 57 | { 58 | Debug.WriteLine("Cache entry for '{0}' has been removed because of {1}.", args.CacheItem.Key, args.RemovedReason); 59 | 60 | if (OnEntryRemoved != null) 61 | { 62 | OnEntryRemoved(args.CacheItem.Key); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CacheR.Model/CacheR.Model.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | 8.0.30703 7 | 2.0 8 | {46613568-995B-4BAF-8566-00D7B8F51351} 9 | Library 10 | Properties 11 | CacheR.Model 12 | CacheR.Model 13 | v4.0 14 | 512 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 53 | -------------------------------------------------------------------------------- /CacheR.Server/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | 5 | namespace CacheR.Server 6 | { 7 | class Program 8 | { 9 | static void Main(string[] args) 10 | { 11 | var listener = new ConsoleTraceListener(); 12 | string url = args.Length == 1 ? args[0] : "http://localhost:8087/"; 13 | 14 | var server = new CacheServer(url); 15 | server.Start(); 16 | 17 | Console.WriteLine("Running cache server on {0}", url); 18 | Console.WriteLine("Press 'q' to quit."); 19 | Console.WriteLine("Press 'v' to view the cache data."); 20 | Console.WriteLine("Press 'd' to enable debug mode."); 21 | 22 | var uri = new Uri(url); 23 | string prompt = String.Format("[{0}:{1}]: ", uri.Host, uri.Port); 24 | 25 | while (true) 26 | { 27 | Console.Write(prompt); 28 | var ki = Console.ReadKey(); 29 | Console.WriteLine(); 30 | if (ki.Key == ConsoleKey.Q) 31 | { 32 | break; 33 | } 34 | 35 | if (ki.Key == ConsoleKey.D) 36 | { 37 | if (Debug.AutoFlush) 38 | { 39 | Debug.Listeners.Remove(listener); 40 | } 41 | else 42 | { 43 | Debug.Listeners.Add(listener); 44 | } 45 | 46 | Debug.AutoFlush = !Debug.AutoFlush; 47 | Log("Turning debugging {0}.", Debug.AutoFlush ? "on" : "off"); 48 | } 49 | 50 | if (ki.Key == ConsoleKey.V) 51 | { 52 | var entries = server.Store.GetAll().Take(100).ToList(); 53 | if (entries.Count == 0) 54 | { 55 | Log("Nothing in the cache."); 56 | } 57 | else 58 | { 59 | foreach (var item in entries) 60 | { 61 | Console.WriteLine(item.Key + " = " + item.Value); 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | private static void Log(string value, params object[] args) 69 | { 70 | Console.WriteLine("[" + DateTime.Now + "]: " + value, args); 71 | } 72 | 73 | private static void Log(string value) 74 | { 75 | Console.WriteLine("[" + DateTime.Now + "]: " + value); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /CacheR.Client.Sample/CacheR.Client.Sample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {2DC48282-150A-42B1-A711-D1C1757D6458} 9 | Exe 10 | Properties 11 | CacheR.Client.Sample 12 | CacheR.Client.Sample 13 | v4.5 14 | 15 | 16 | 512 17 | ..\ 18 | true 19 | 20 | 21 | x86 22 | true 23 | full 24 | false 25 | bin\Debug\ 26 | DEBUG;TRACE 27 | prompt 28 | 4 29 | false 30 | 31 | 32 | x86 33 | pdbonly 34 | true 35 | bin\Release\ 36 | TRACE 37 | prompt 38 | 4 39 | false 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C} 53 | CacheR.Client 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 68 | -------------------------------------------------------------------------------- /CacheR.Client/CacheR.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | AnyCPU 6 | 8.0.30703 7 | 2.0 8 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C} 9 | Library 10 | Properties 11 | CacheR.Client 12 | CacheR.Client 13 | v4.5 14 | 512 15 | ..\ 16 | true 17 | 18 | 19 | 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | false 28 | 29 | 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | false 37 | 38 | 39 | 40 | ..\packages\Microsoft.AspNet.SignalR.Client.2.0.0-rc1\lib\net45\Microsoft.AspNet.SignalR.Client.dll 41 | 42 | 43 | False 44 | ..\packages\Newtonsoft.Json.5.0.6\lib\net45\Newtonsoft.Json.dll 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {46613568-995B-4BAF-8566-00D7B8F51351} 57 | CacheR.Model 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 72 | -------------------------------------------------------------------------------- /CacheR.Client/CacheClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Threading.Tasks; 4 | using CacheR.Model; 5 | using Microsoft.AspNet.SignalR.Client; 6 | using Newtonsoft.Json; 7 | 8 | namespace CacheR.Client 9 | { 10 | public class Cache 11 | { 12 | private readonly Connection _connection; 13 | private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); 14 | 15 | public Cache(string server) 16 | { 17 | if (!server.EndsWith("/")) 18 | { 19 | server += "/"; 20 | } 21 | 22 | _connection = new Connection(server + "cache"); 23 | _connection.Received += OnCacheEntryReceived; 24 | } 25 | 26 | private void OnCacheEntryReceived(string data) 27 | { 28 | var command = JsonConvert.DeserializeObject(data); 29 | 30 | switch (command.Type) 31 | { 32 | case CacheCommandType.Add: 33 | foreach (var entry in command.Entries) 34 | { 35 | // You can't trick me C#... 36 | object value = entry.Value; 37 | _cache.AddOrUpdate(entry.Key, entry.Value, (k, v) => value); 38 | } 39 | break; 40 | case CacheCommandType.Remove: 41 | foreach (var entry in command.Entries) 42 | { 43 | object value; 44 | _cache.TryRemove(entry.Key, out value); 45 | } 46 | break; 47 | default: 48 | break; 49 | } 50 | } 51 | 52 | public object Get(string key) 53 | { 54 | object value; 55 | if (_cache.TryGetValue(key, out value)) 56 | { 57 | return value; 58 | } 59 | 60 | return null; 61 | } 62 | 63 | public Task AddAsync(string key, object value) 64 | { 65 | var entry = new[] { 66 | new CacheEntry 67 | { 68 | Key = key, 69 | Value = value 70 | } 71 | }; 72 | 73 | var command = new CacheCommand 74 | { 75 | Type = CacheCommandType.Add, 76 | Entries = entry 77 | }; 78 | 79 | // Make it available immediately to the local cache 80 | _cache.AddOrUpdate(key, value, (k, v) => value); 81 | 82 | return SendCommand(command); 83 | } 84 | 85 | public Task DeleteAsync(string key) 86 | { 87 | var command = new CacheCommand 88 | { 89 | Type = CacheCommandType.Remove, 90 | Entries = new[] { 91 | new CacheEntry { 92 | Key = key 93 | } 94 | } 95 | }; 96 | 97 | // Execute local delete 98 | object value; 99 | _cache.TryRemove(key, out value); 100 | 101 | return SendCommand(command); 102 | } 103 | 104 | public Task ConnectAsync() 105 | { 106 | return _connection.Start(); 107 | } 108 | 109 | private Task SendCommand(CacheCommand command) 110 | { 111 | return _connection.Send(JsonConvert.SerializeObject(command)); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 8 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) 9 | $([System.IO.Path]::Combine($(SolutionDir), "packages")) 10 | 11 | 12 | $(SolutionDir).nuget 13 | packages.config 14 | $(SolutionDir)packages 15 | 16 | 17 | $(NuGetToolsPath)\nuget.exe 18 | "$(NuGetExePath)" 19 | mono --runtime=v4.0.30319 $(NuGetExePath) 20 | 21 | $(TargetDir.Trim('\\')) 22 | 23 | 24 | "" 25 | 26 | 27 | false 28 | 29 | 30 | false 31 | 32 | 33 | $(NuGetCommand) install "$(PackagesConfig)" -source $(PackageSources) -o "$(PackagesDir)" 34 | $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols 35 | 36 | 37 | 38 | RestorePackages; 39 | $(BuildDependsOn); 40 | 41 | 42 | 43 | 44 | $(BuildDependsOn); 45 | BuildPackage; 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 61 | 62 | 63 | 64 | 66 | 67 | 70 | 71 | -------------------------------------------------------------------------------- /CacheR.Server/CacheServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CacheR.Model; 6 | using Microsoft.AspNet.SignalR; 7 | using Microsoft.AspNet.SignalR.Infrastructure; 8 | using Microsoft.Owin.Hosting; 9 | using Newtonsoft.Json; 10 | using Owin; 11 | 12 | namespace CacheR.Server 13 | { 14 | public class CacheServer 15 | { 16 | private IDisposable _server; 17 | private IPersistentConnectionContext _context; 18 | private readonly string _url; 19 | 20 | public CacheServer(string url) 21 | : this(new MemoryCacheStore(), url) 22 | { 23 | } 24 | 25 | public CacheServer(ICacheStore store, string url) 26 | { 27 | Store = store; 28 | Store.OnEntryRemoved = OnEntryRemoved; 29 | _url = url; 30 | } 31 | 32 | public ICacheStore Store 33 | { 34 | get; 35 | private set; 36 | } 37 | 38 | public void Start() 39 | { 40 | if (_server == null) 41 | { 42 | _server = WebApp.Start(_url, app => 43 | { 44 | var config = new ConnectionConfiguration 45 | { 46 | Resolver = new DefaultDependencyResolver() 47 | }; 48 | 49 | config.Resolver.Register(typeof(CacheConnection), () => new CacheConnection(this)); 50 | 51 | _context = config.Resolver.Resolve().GetConnectionContext(); 52 | 53 | app.MapSignalR("/cache", config); 54 | }); 55 | } 56 | } 57 | 58 | public void Stop() 59 | { 60 | if (_server != null) 61 | { 62 | _server.Dispose(); 63 | _server = null; 64 | } 65 | } 66 | 67 | private Task Save(string rawCommand) 68 | { 69 | Debug.WriteLine("Processing command: " + rawCommand); 70 | 71 | // REVIEW: Should save retry on failure? 72 | var command = JsonConvert.DeserializeObject(rawCommand); 73 | 74 | switch (command.Type) 75 | { 76 | case CacheCommandType.Add: 77 | return Store.Save(command.Entries[0]); 78 | case CacheCommandType.Remove: 79 | return Store.Delete(command.Entries[0].Key); 80 | default: 81 | throw new NotSupportedException(); 82 | } 83 | } 84 | 85 | public void OnEntryRemoved(string key) 86 | { 87 | var command = new CacheCommand 88 | { 89 | Type = CacheCommandType.Remove, 90 | Entries = new[] { 91 | new CacheEntry { 92 | Key = key 93 | } 94 | } 95 | }; 96 | 97 | // When an entry is removed let all subscribers know 98 | _context.Connection.Broadcast(command); 99 | } 100 | 101 | private class CacheConnection : PersistentConnection 102 | { 103 | private readonly CacheServer _cache; 104 | 105 | public CacheConnection(CacheServer cache) 106 | { 107 | _cache = cache; 108 | } 109 | 110 | protected override Task OnConnected(IRequest request, string connectionId) 111 | { 112 | var command = new CacheCommand 113 | { 114 | Type = CacheCommandType.Add, 115 | Entries = _cache.Store.GetAll().ToArray() 116 | }; 117 | 118 | // REVIEW: Should we send all of the data to the client on reconnect? 119 | // Let the client request a subset of the data or none at all. 120 | return Connection.Send(connectionId, command); 121 | } 122 | 123 | protected override async Task OnReceived(IRequest request, string connectionId, string data) 124 | { 125 | // Store the data and tell all clients to update 126 | await _cache.Save(data); 127 | await Connection.Broadcast(data); 128 | } 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /CacheR.Server/CacheR.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Debug 5 | x86 6 | 8.0.30703 7 | 2.0 8 | {88B15858-B9DF-4CCA-A963-4BD888456D96} 9 | Exe 10 | Properties 11 | CacheR.Server 12 | CacheR.Server 13 | v4.5 14 | 15 | 16 | 512 17 | ..\ 18 | true 19 | 20 | 21 | x86 22 | true 23 | full 24 | false 25 | bin\Debug\ 26 | DEBUG;TRACE 27 | prompt 28 | 4 29 | false 30 | 31 | 32 | x86 33 | pdbonly 34 | true 35 | bin\Release\ 36 | TRACE 37 | prompt 38 | 4 39 | false 40 | 41 | 42 | 43 | ..\packages\Microsoft.AspNet.SignalR.Core.2.0.0-rc1\lib\net45\Microsoft.AspNet.SignalR.Core.dll 44 | 45 | 46 | ..\packages\Microsoft.Owin.2.0.0-rc1\lib\net45\Microsoft.Owin.dll 47 | 48 | 49 | ..\packages\Microsoft.Owin.Diagnostics.2.0.0-rc1\lib\net40\Microsoft.Owin.Diagnostics.dll 50 | 51 | 52 | ..\packages\Microsoft.Owin.Host.HttpListener.2.0.0-rc1\lib\net45\Microsoft.Owin.Host.HttpListener.dll 53 | 54 | 55 | ..\packages\Microsoft.Owin.Hosting.2.0.0-rc1\lib\net45\Microsoft.Owin.Hosting.dll 56 | 57 | 58 | ..\packages\Microsoft.Owin.Security.2.0.0-rc1\lib\net45\Microsoft.Owin.Security.dll 59 | 60 | 61 | False 62 | ..\packages\Newtonsoft.Json.5.0.6\lib\net45\Newtonsoft.Json.dll 63 | 64 | 65 | ..\packages\Owin.1.0\lib\net40\Owin.dll 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | {46613568-995B-4BAF-8566-00D7B8F51351} 85 | CacheR.Model 86 | 87 | 88 | 89 | 90 | 97 | -------------------------------------------------------------------------------- /CacheR.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual Studio 2010 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheR.Server", "CacheR.Server\CacheR.Server.csproj", "{88B15858-B9DF-4CCA-A963-4BD888456D96}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheR.Client.Sample", "CacheR.Client.Sample\CacheR.Client.Sample.csproj", "{2DC48282-150A-42B1-A711-D1C1757D6458}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheR.Client", "CacheR.Client\CacheR.Client.csproj", "{A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CacheR.Model", "CacheR.Model\CacheR.Model.csproj", "{46613568-995B-4BAF-8566-00D7B8F51351}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{AA30A3FC-720E-41D8-8FD7-3D1B7658CB4E}" 13 | ProjectSection(SolutionItems) = preProject 14 | .nuget\NuGet.exe = .nuget\NuGet.exe 15 | .nuget\NuGet.targets = .nuget\NuGet.targets 16 | EndProjectSection 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|Mixed Platforms = Debug|Mixed Platforms 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|Mixed Platforms = Release|Mixed Platforms 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {88B15858-B9DF-4CCA-A963-4BD888456D96}.Debug|Any CPU.ActiveCfg = Debug|x86 29 | {88B15858-B9DF-4CCA-A963-4BD888456D96}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 30 | {88B15858-B9DF-4CCA-A963-4BD888456D96}.Debug|Mixed Platforms.Build.0 = Debug|x86 31 | {88B15858-B9DF-4CCA-A963-4BD888456D96}.Debug|x86.ActiveCfg = Debug|x86 32 | {88B15858-B9DF-4CCA-A963-4BD888456D96}.Debug|x86.Build.0 = Debug|x86 33 | {88B15858-B9DF-4CCA-A963-4BD888456D96}.Release|Any CPU.ActiveCfg = Release|x86 34 | {88B15858-B9DF-4CCA-A963-4BD888456D96}.Release|Mixed Platforms.ActiveCfg = Release|x86 35 | {88B15858-B9DF-4CCA-A963-4BD888456D96}.Release|Mixed Platforms.Build.0 = Release|x86 36 | {88B15858-B9DF-4CCA-A963-4BD888456D96}.Release|x86.ActiveCfg = Release|x86 37 | {88B15858-B9DF-4CCA-A963-4BD888456D96}.Release|x86.Build.0 = Release|x86 38 | {2DC48282-150A-42B1-A711-D1C1757D6458}.Debug|Any CPU.ActiveCfg = Debug|x86 39 | {2DC48282-150A-42B1-A711-D1C1757D6458}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 40 | {2DC48282-150A-42B1-A711-D1C1757D6458}.Debug|Mixed Platforms.Build.0 = Debug|x86 41 | {2DC48282-150A-42B1-A711-D1C1757D6458}.Debug|x86.ActiveCfg = Debug|x86 42 | {2DC48282-150A-42B1-A711-D1C1757D6458}.Debug|x86.Build.0 = Debug|x86 43 | {2DC48282-150A-42B1-A711-D1C1757D6458}.Release|Any CPU.ActiveCfg = Release|x86 44 | {2DC48282-150A-42B1-A711-D1C1757D6458}.Release|Mixed Platforms.ActiveCfg = Release|x86 45 | {2DC48282-150A-42B1-A711-D1C1757D6458}.Release|Mixed Platforms.Build.0 = Release|x86 46 | {2DC48282-150A-42B1-A711-D1C1757D6458}.Release|x86.ActiveCfg = Release|x86 47 | {2DC48282-150A-42B1-A711-D1C1757D6458}.Release|x86.Build.0 = Release|x86 48 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 51 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 52 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}.Debug|x86.ActiveCfg = Debug|Any CPU 53 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 56 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}.Release|Mixed Platforms.Build.0 = Release|Any CPU 57 | {A02EDCA2-6BB3-4AC9-8DDE-63D9BBAA866C}.Release|x86.ActiveCfg = Release|Any CPU 58 | {46613568-995B-4BAF-8566-00D7B8F51351}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {46613568-995B-4BAF-8566-00D7B8F51351}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {46613568-995B-4BAF-8566-00D7B8F51351}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 61 | {46613568-995B-4BAF-8566-00D7B8F51351}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 62 | {46613568-995B-4BAF-8566-00D7B8F51351}.Debug|x86.ActiveCfg = Debug|Any CPU 63 | {46613568-995B-4BAF-8566-00D7B8F51351}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {46613568-995B-4BAF-8566-00D7B8F51351}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {46613568-995B-4BAF-8566-00D7B8F51351}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 66 | {46613568-995B-4BAF-8566-00D7B8F51351}.Release|Mixed Platforms.Build.0 = Release|Any CPU 67 | {46613568-995B-4BAF-8566-00D7B8F51351}.Release|x86.ActiveCfg = Release|Any CPU 68 | EndGlobalSection 69 | GlobalSection(SolutionProperties) = preSolution 70 | HideSolutionNode = FALSE 71 | EndGlobalSection 72 | EndGlobal 73 | --------------------------------------------------------------------------------