Toto
Toto

Reputation: 7719

C# generic cache with type safe

I'm looking for a way to have a generic local cache for any object. Here is the code :

    private static readonly Dictionary<Type,Dictionary<string,object>> _cache 
        = new Dictionary<Type, Dictionary<string, object>>();

    //The generic parameter allow null values to be cached
    private static void AddToCache<T>(string key, T value)
    {
        if(!_cache.ContainsKey(typeof(T)))
            _cache.Add(typeof(T),new Dictionary<string, object>());

        _cache[typeof (T)][key] = value;
    }

    private static T GetFromCache<T>(string key)
    {
        return (T)_cache[typeof (T)][key];
    }   

1- Is there a way not to cast on the getfromcache method ?

2- Is there a way to ensure type safe in the second dictionnary, say all the object would have the same type. (This is provided by the addToCache method but I would prefer a type control in the design himself). For eg to have _cache of the following type

    Dictionary<Type,Dictionary<string,typeof(type)>>

Thx

Upvotes: 3

Views: 6751

Answers (7)

CajunCoding
CajunCoding

Reputation: 969

Even though this is an older thread, it popped up in a search I ran today... here's an updated version of the post from @Daniel, that implements thread safety (as noted in multiple good comments) as well as self-populating/lazy cache initialization:

public static class SimpleLazyCacheHelper<T>
{
    private static readonly ConcurrentDictionary<string, Lazy<T>> cache = new ConcurrentDictionary<string, Lazy<T>>();

    [Obsolete("This method adds existing values to the cache, but it's best to use GetOrAddToCache() to facilitate a self-populating cache (especially in a Web environment)")]
    public static void AddToCache(string key, T value)
    {
        cache.TryAdd(key, new Lazy<T>(() => value));
    }

    [Obsolete("This method returns only existing values; it's best to initialize cache values via GetOrAddToCache()")]
    public static T GetFromCache(string key)
    {
        return cache[key].Value;
    }

    /// <summary>
    /// Provides a self-populating/blocking approach to the cache so that ALL Threads wait for the first thread that requested a cache key
    ///     to be initialized and populated into the cache. This means that they will always wait less time than if they did all the
    ///     work itself.  Very useful for long running work (e.g. DB calls, I/O processing, etc).
    /// More info. on the value fo self-populating approach is here:
    ///     https://www.ehcache.org/documentation/2.8/apis/constructs.html
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="key"></param>
    /// <param name="valueFactory"></param>
    public static T GetOrAddToCache(string key, Func<T> valueFactory)
    {
        return cache.GetOrAdd(key, new Lazy<T>(valueFactory)).Value;
    }
}

Usage is:

CacheHelper<MyCacheableClass>.GetOrAddToCache($"UniqueDynamicKey", () =>
{
     //
     //... do some long running work...
     //
     return new MyCacheableClass();
});

Also provided as a Gist here: https://gist.github.com/cajuncoding/e2ff490d79812c83bedfd5a77888e727

And, for anyone looking for a fully fleshed out solution we use in production you can see this same pattern used in my LazyCachHelpers project here: https://github.com/cajuncoding/LazyCacheHelpers

Upvotes: 0

Chris Marisic
Chris Marisic

Reputation: 33118

IMemoryCache

bootstrapping:

services.AddMemoryCache();

usage:

public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;

        var onlineAt = _cache.Get<DateTime?>("self:startup");
    }

For writes and bootstrapping details check the link above. It combines much of what my answer included. You can also use Microsoft.Extensions.Caching.Redis or other distributed caches.

This is now the 'official' approach.


Original answer for posterity

I have to throw this out there, this doesn't really seem like something you should be doing. Whatever you are doing most likely could be much better achieved by either using a Dependency Injection / Inversion of Control Library, a tool like Enterprise Library CacheManager or using a distributed memory caching program such as Memcache or Microsoft Velocity.

Upvotes: 2

Matthew Whited
Matthew Whited

Reputation: 22443

should probably toss in a few locks and maybe a few clones... but this should do.

class Program
{
    static void Main(string[] args)
    {
        CacheObject<int>.Instance["hello"] = 5;
        CacheObject<int>.Instance["hello2"] = 6;
        CacheObject<string>.Instance["hello2"] = "hi";
        Console.WriteLine(CacheObject<string>.Instance["hello2"]); //returns hi
    }
}

public class CacheObject<V> : CacheObject<string, V> { }
public class CacheObject<K,V>
{
    private static CacheObject<K, V> _instance = new CacheObject<K, V>();
    public static CacheObject<K, V> Instance { get { return _instance; } }

    private Dictionary<K, V> _store = new Dictionary<K, V>();
    public T this[K index]
    {
        get { return _store.ContainsKey(index) ? _store[index] : default(V); }
        set
        {
            if (_store.ContainsKey(index)) _store.Remove(index);
            if (value != null) _store.Add(index, value);
        }
    }
}

Upvotes: 0

Stefan Steinegger
Stefan Steinegger

Reputation: 64638

You don't gain much with this, unless that primitive types are not boxed.

private static readonly Dictionary<Type,Dictionary<string,object>> _cache 
    = new Dictionary<Type, IDictionary>();

//The generic parameter allow null values to be cached
private static void AddToCache<T>(string key, T value)
{
    // create a dictionary of the correct type
    if(!_cache.ContainsKey(typeof(T)))
        _cache.Add(typeof(T),new Dictionary<string, T>());

    _cache[typeof (T)][key] = value;
}

private static T GetFromCache<T>(string key)
{
    // casting the dictionary instead of the value
    Dictionary<string, T> typedDictionary = (Dictionary<string, T>)_cache[typeof (T)];
    return typedDictionary[key];
}

Of course it needs more not-found handling.

Upvotes: 1

Filmund
Filmund

Reputation: 71

You won't be able to create a cache that is strongly typed if you don't know the required type at compile time. If you don't know the required type until run time then the answers to your questions are no and no, I'm afraid.

Upvotes: 0

Dmitry Ornatsky
Dmitry Ornatsky

Reputation: 2237

Why not just put generic parameter to class declaration:

public class Cache<T>
{
    private Dictionary<string, T> _cache = new Dictionary<string, T>();
    ...

}

it can be static if you prefer

Upvotes: 3

Daniel
Daniel

Reputation: 16464

Try this:

static class Helper<T>
{
       internal static readonly Dictionary<string, T> cache = new Dictionary<string, T>();
}
private static void AddToCache<T>(string key, T value)
{
   Helper<T>.cache[key] = value;
}
private static T GetFromCache<T>(string key)
{
    return Helper<T>.cache[key];
}

Upvotes: 12

Related Questions