Created
December 7, 2018 23:10
-
-
Save pmunin/f0833d32ddadd8985dedb7fcac710dc3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.ComponentModel.DataAnnotations; | |
using System.ComponentModel.DataAnnotations.Schema; | |
namespace Caching{ | |
[Table(DbCacheService.TableName)] | |
public class CachedValue | |
{ | |
[Key] | |
[StringLength(250)] | |
public string Key { get;set; } | |
[Key] | |
[StringLength(250)] | |
public string Namespace { get; set; } | |
public string Value { get;set; } | |
[Column(TypeName = "datetime")] | |
public DateTime LastUpdate { get;set; } | |
public DateTime? Expires { get; set; } | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Concurrent; | |
using System.Data.SqlClient; | |
using System.Linq; | |
using System.Threading; | |
using Castle.Windsor; | |
using Microsoft.EntityFrameworkCore; | |
namespace Caching | |
{ | |
public class DbCacheService : ICacheService | |
{ | |
public const string TableName = "Cache"; | |
public IWindsorContainer Dependencies { get; set; } | |
public string Namespace { get; } | |
public DbCacheService(string @namespace=null) | |
{ | |
this.Namespace = @namespace??""; | |
} | |
public TValue Get<TKey, TValue>(TKey key, Func<TKey, TValue> generateIfNotExist) | |
{ | |
var saveChanges = true; | |
using (var db = Dependencies.Resolve<CacheDbContext>()) | |
{ | |
var keyStr = KeyToString(key); | |
using (ThreadSafeUpdatingCacheByKey(keyStr, Namespace))//TODO: should be ReaderWriterLockSlim instead of just Monitor | |
{ | |
var cached = db.CachedValues.FirstOrDefault(c=>c.Key==keyStr&&c.Namespace==Namespace); | |
TValue val; | |
if (cached != null) | |
{ | |
if (!TryValueFromString<TValue>(cached.Value, out val)) | |
{ | |
throw new InvalidOperationException( | |
$"Could not deserialize {typeof(TValue).Name} from '{cached.Value}'"); | |
} | |
} | |
else | |
{ | |
val = generateIfNotExist(key); | |
cached = new CachedValue() {Key = keyStr, Namespace = Namespace, Value = ValueToString(val), LastUpdate = DateTime.Now }; | |
db.CachedValues.Add(cached); | |
if (saveChanges) | |
{ | |
db.SaveChanges(); | |
} | |
} | |
return val; | |
} | |
} | |
} | |
public TValue Set<TKey, TValue>( | |
TKey key, | |
Func<TKey, TValue> getNewValue, | |
Func<TKey, TValue, TValue> updateValue=null | |
) | |
{ | |
var saveChanges = true; | |
using (var db = Dependencies.Resolve<CacheDbContext>()) | |
{ | |
var keyStr = KeyToString(key); | |
using (ThreadSafeUpdatingCacheByKey(keyStr, Namespace)) | |
{ | |
var cached = db.CachedValues.FirstOrDefault(c=>c.Key==keyStr&&c.Namespace==Namespace); | |
var isInCache = true; | |
if (cached == null) | |
{ | |
isInCache = false; | |
cached = new CachedValue() {Key = keyStr, Namespace = Namespace}; | |
db.CachedValues.Add(cached); | |
} | |
var valStr = cached?.Value; | |
TryValueFromString<TValue>(valStr, out var val); | |
val = isInCache | |
? updateValue != null | |
? updateValue(key, val) | |
: getNewValue(key) | |
: getNewValue(key); | |
valStr = ValueToString(val); | |
cached.Value = valStr; | |
cached.LastUpdate = DateTime.Now; | |
if (saveChanges) | |
{ | |
db.SaveChanges(); | |
} | |
return val; | |
} | |
} | |
} | |
public void Remove<TKey>(TKey key) | |
{ | |
var saveChanges = true; | |
using (var db = Dependencies.Resolve<CacheDbContext>()) | |
{ | |
var keyStr = KeyToString(key); | |
using (ThreadSafeUpdatingCacheByKey(keyStr, Namespace)) | |
{ | |
var cached = db.CachedValues.FirstOrDefault(c=>c.Key==keyStr&&c.Namespace==Namespace); | |
if (cached != null) | |
{ | |
db.CachedValues.Remove(cached); | |
if (saveChanges) | |
{ | |
db.SaveChanges(); | |
} | |
} | |
} | |
} | |
} | |
public void Clear() | |
{ | |
var saveChanges = true; | |
using (var db = Dependencies.Resolve<CacheDbContext>()) | |
{ | |
//TODO: should be working through parameter | |
var len = Namespace.Length; | |
db.Database.ExecuteSqlCommand($"delete from dbo.cache where Left([namespace],{len}) = @ns", new SqlParameter("@ns",Namespace)); | |
} | |
} | |
static IDisposable ThreadSafeUpdatingCacheByKey(string keyStr, string ns) | |
{ | |
var sync = lockByKey.GetOrAdd((key: keyStr, ns :ns), k => (locker : new Object(), count: 0)); | |
Interlocked.Increment(ref sync.count); | |
Monitor.Enter(sync.locker); | |
return Disposable.For(() => | |
{ | |
Interlocked.Decrement(ref sync.count); | |
if (sync.count <= 0) | |
{ | |
lockByKey.TryRemove((key:keyStr, ns:ns), out var foo); | |
} | |
}); | |
} | |
static ConcurrentDictionary<(string key, string ns), (object locker, int count)> lockByKey = new ConcurrentDictionary<(string key, string ns), (object locker, int count)>(); | |
static string KeyToString(object key) | |
{ | |
var keySerializedStr = Newtonsoft.Json.JsonConvert.SerializeObject(key); | |
//TODO: take care of collisions - array can be represented as {Args[0].GetHashCode().ToString()}_{Args[1].GetHashCode().ToString()}_... | |
return keySerializedStr.GetHashCode().ToString(); | |
} | |
static string ValueToString<T>(T value) | |
{ | |
return Newtonsoft.Json.JsonConvert.SerializeObject(value); | |
} | |
static bool TryValueFromString<T>(string value, out T res) | |
{ | |
res = default(T); | |
try | |
{ | |
res = Newtonsoft.Json.JsonConvert.DeserializeObject<T>(value); | |
return true; | |
} | |
catch (Exception e) | |
{ | |
return false; | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
namespace Caching | |
{ | |
/// <summary> | |
/// Service that allows to manipulate cache | |
/// </summary> | |
public interface ICacheService | |
{ | |
/// <summary> | |
/// Gets or add value to the cache by key | |
/// </summary> | |
/// <typeparam name="TKey"></typeparam> | |
/// <typeparam name="TValue"></typeparam> | |
/// <param name="key"></param> | |
/// <param name="generateIfNotExist"></param> | |
/// <returns></returns> | |
TValue Get<TKey, TValue>(TKey key, Func<TKey, TValue> generateIfNotExist); | |
/// <summary> | |
/// Updates the cache for key | |
/// </summary> | |
/// <typeparam name="TKey"></typeparam> | |
/// <typeparam name="TValue"></typeparam> | |
/// <param name="key">cache record serializable key (e.g. function name with parameter values)</param> | |
/// <param name="getNewValue">generates new value IF value is not in cache OR updateValue parameter is not specified(=null)</param> | |
/// <param name="updateValue">if specified - should should return updated value, based on current value stored in cache | |
/// if not specified, then getNewValue is invoked | |
/// </param> | |
TValue Set<TKey, TValue>(TKey key, Func<TKey, TValue> getNewValue, Func<TKey, TValue, TValue> updateValue=null); | |
/// <summary> | |
/// Removes cache record by key if it exists | |
/// </summary> | |
/// <typeparam name="TKey"></typeparam> | |
/// <param name="key"></param> | |
void Remove<TKey>(TKey key); | |
/// <summary> | |
/// Clears all cache records | |
/// </summary> | |
void Clear(); | |
} | |
public static class CacheServiceExtensions | |
{ | |
/// <summary> | |
/// Just sets specified value in the cache overwriting current cache value if it exists in cache | |
/// </summary> | |
/// <typeparam name="TKey"></typeparam> | |
/// <typeparam name="TValue"></typeparam> | |
/// <param name="key">cache key</param> | |
/// <param name="newValue">new value</param> | |
public static ICacheService Set<TKey, TValue>(this ICacheService cache, TKey key, TValue newValue) | |
{ | |
cache.Set(key, k => newValue); | |
return cache; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment