Skip to content

Instantly share code, notes, and snippets.

@pmunin
Created December 7, 2018 23:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pmunin/f0833d32ddadd8985dedb7fcac710dc3 to your computer and use it in GitHub Desktop.
Save pmunin/f0833d32ddadd8985dedb7fcac710dc3 to your computer and use it in GitHub Desktop.
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; }
}
}
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;
}
}
}
}
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