Skip to content

Instantly share code, notes, and snippets.

@DamianSuess
Last active August 29, 2023 20:20
Show Gist options
  • Save DamianSuess/55aa7d94cfa6074fa68c80484121c2ba to your computer and use it in GitHub Desktop.
Save DamianSuess/55aa7d94cfa6074fa68c80484121c2ba to your computer and use it in GitHub Desktop.
C# CacheService
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using CacheExample.Extensions;
namespace CacheExample.Services;
/// <summary>
/// Cache service.
/// Recommended as a Transient; choose your DI registration carefully.
/// * Transient - A new instance is created with every retrieval of the service.
/// * Scoped - Operations change only w/ a new scope.
/// * Singleton - A new instance is only created once.
/// </summary>
/// <typeparam name="TItem">Type of cache.</typeparam>
public class CacheService<TItem> : ICacheService<TItem>
{
private MemoryCache _cache;
private ConcurrentDictionary<object, SemaphoreSlim> _locks = new();
public CacheService()
{
var options = new MemoryCacheOptions
{
SizeLimit = 1024,
};
_cache = new MemoryCache(options);
// Remove item from cache if not accessed for 5 minutes
ExpirationSliding = TimeSpan.FromMinutes(5);
// Remove item from cache after 10 minutes if it wasn't already
ExpirationAbsolute = TimeSpan.FromMinutes(10);
}
/// <summary>
/// Gets or sets the absolute time to remove item
/// from cache after the specified amount of time.
/// </summary>
public TimeSpan ExpirationAbsolute { get; set; }
/// <summary>
/// Gets or sets the sliding time to keep item cached
/// and reset time if accessed during this period of time.
/// </summary>
public TimeSpan ExpirationSliding { get; set; }
/// <summary>Get item value from cache.</summary>
/// <param name="key">Key of the cached value to retrieve.</param>
/// <returns>Cached item or null.</returns>
public TItem Get(object key) => _cache.Get<TItem>(key);
/// <summary>Gets item from cache or creates a new entry if it does not exist.</summary>
/// <param name="key">Key.</param>
/// <param name="newItem">Object item to cache.</param>
/// <returns>Item from cache.</returns>
public TItem Get(object key, Func<TItem> newItem)
{
if (!_cache.TryGetValue(key, out TItem cache))
{
cache = newItem();
var entryOptions = new MemoryCacheEntryOptions()
.SetSize(1)
.SetPriority(CacheItemPriority.High)
.SetSlidingExpiration(ExpirationSliding)
.SetAbsoluteExpiration(ExpirationAbsolute);
_cache.Set(key, cache, entryOptions);
}
// Alt method.
////_cache.GetOrCreate(key, entry =>
////{
//// //// entry.AbsoluteExpiration = DateTimeOffset.
//// entry.SlidingExpiration = ExpirationSliding;
//// return Task.FromResult(DateTime.Now);
////});
return cache;
}
/// <summary>
/// Asynchronously gets item from cache or creates a new entry if
/// it does not exist. Use <see cref="GetAsync"/> when fetching
/// operations take a long time - i.e. DB lookups.
/// </summary>
/// <param name="key">Key.</param>
/// <param name="newItem">Object item to cache.</param>
/// <returns>Item from cache.</returns>
public async Task<TItem> GetAsync(object key, Func<Task<TItem>> newItem)
{
if (!_cache.TryGetValue(key, out TItem cache))
{
SemaphoreSlim slimLock = _locks.GetOrAdd(key, k => new SemaphoreSlim(1, 1));
await slimLock.WaitAsync();
try
{
// Yes, do it again.
if (!_cache.TryGetValue(key, out cache))
{
cache = await newItem();
var entryOptions = new MemoryCacheEntryOptions()
.SetSize(1)
.SetPriority(CacheItemPriority.High)
.SetSlidingExpiration(ExpirationSliding)
.SetAbsoluteExpiration(ExpirationAbsolute);
_cache.Set(key, cache, entryOptions);
}
}
finally
{
slimLock.Release();
}
}
return cache;
}
/// <summary>Returns a collection of MemoryCache items.</summary>
/// <returns>Memory cache collection.</returns>
public IEnumerable Keys() => _cache.GetKeys();
/// <summary>Return cache of keys given the specified type.</summary>
/// <typeparam name="T">Type of Cache Keys.</typeparam>
/// <returns>Returns all keys.</returns>
public IEnumerable Keys<T>() => _cache.GetKeys<T>();
}
using System;
namespace CacheExample;
public class UsageExample
{
private readonly ICacheService<int> _eventCache;
public UsageExample()
{
_eventCache = new CacheService<int>();
}
public Dictionary<DateTime, string>()
{
get
{
var e = new Dictionary<DateTime, string>();
foreach (var itemKey in _eventCache.Keys())
{
var key = (DateTime)itemKey;
e.Add(key, _eventCache.Get(itemKey);
}
}
}
public void AddItem(string someEvent))
{
_eventCache.Get(DateTime.Now, () => someEvent);
}
}
using System;
using System.Collections;
using System.Threading.Tasks;
namespace CacheExample.Services;
/// <summary>Interface for Caching system.</summary>
/// <typeparam name="TItem">Data object type to cache.</typeparam>
/// <example>
/// <code><![CDATA[
/// var cache = new MemoryCache(new MemoryCacheOptions());
/// cache.GetOrCreate(1, ce => "one");
/// cache.GetOrCreate("two", ce => "two");
///
/// foreach (var key in cache.GetKeys())
/// Console.WriteLine($"Key: '{key}', Key type: '{key.GetType()}'");
///
/// foreach (var key in cache.GetKeys<string>())
/// Console.WriteLine($"Key: '{key}', Key type: '{key.GetType()}'");
///
/// --[ OUTPUT ]--
/// Key: '1', Key type: 'System.Int32'
/// Key: 'two', Key type: 'System.String'
/// Key: 'two', Key type: 'System.String'
/// ]]></code>
/// </example>
public interface ICacheService<TItem>
{
/// <summary>
/// Gets or sets the absolute time to remove item
/// from cache after the specified amount of time.
/// </summary>
TimeSpan ExpirationAbsolute { get; set; }
/// <summary>
/// Gets or sets the sliding time to keep item cached
/// and reset time if accessed during this period of time.
/// </summary>
TimeSpan ExpirationSliding { get; set; }
TItem Get(object key);
TItem Get(object key, Func<TItem> newItem);
Task<TItem> GetAsync(object key, Func<Task<TItem>> newItem);
/// <summary>Returns a collection of MemoryCache items.</summary>
/// <returns>Memory cache collection.</returns>
IEnumerable Keys();
/// <summary>Return cache of keys given the specified type.</summary>
/// <typeparam name="T">Type of Cache Keys.</typeparam>
/// <returns>Returns all keys.</returns>
IEnumerable Keys<T>();
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Microsoft.Extensions.Caching.Memory;
namespace CacheExample.Extensions;
/// <summary>
/// Add the ability to retrieve keys from MemoryCache.
/// </summary>
/// <example>
/// <code><![CDATA[
/// var cache = new MemoryCache(new MemoryCacheOptions());
/// cache.GetOrCreate(1, ce => "one");
/// cache.GetOrCreate("two", ce => "two");
///
/// foreach (var key in cache.GetKeys())
/// Console.WriteLine($"Key: '{key}', Key type: '{key.GetType()}'");
///
/// foreach (var key in cache.GetKeys<string>())
/// Console.WriteLine($"Key: '{key}', Key type: '{key.GetType()}'");
///
/// --[ OUTPUT ]--
/// Key: '1', Key type: 'System.Int32'
/// Key: 'two', Key type: 'System.String'
/// Key: 'two', Key type: 'System.String'
/// ]]></code>
/// </example>
public static class MemoryCacheExtension
{
/// <summary>Get MemoryCache's private CoherentState field where entries are located.</summary>
private static readonly Lazy<Func<MemoryCache, object>> GetCoherentState =
new(() =>
CreateGetter<MemoryCache, object>(typeof(MemoryCache)
.GetField("_coherentState", BindingFlags.NonPublic | BindingFlags.Instance)));
private static readonly Func<MemoryCache, IDictionary> GetEntries =
Assembly.GetAssembly(typeof(MemoryCache)).GetName().Version.Major < 7
? (Func<MemoryCache, IDictionary>)(cache => (IDictionary)GetEntries6.Value(cache))
: cache => GetEntries7.Value(GetCoherentState.Value(cache));
/// <summary>Microsoft.Extensions.Caching.Memory.dll v6.0.</summary>
private static readonly Lazy<Func<MemoryCache, object>> GetEntries6 = new(
() => (Func<MemoryCache, object>)Delegate.CreateDelegate(
typeof(Func<MemoryCache, object>),
typeof(MemoryCache).GetProperty("EntriesCollection", BindingFlags.NonPublic | BindingFlags.Instance).GetGetMethod(true),
throwOnBindFailure: true));
/// <summary>Microsoft.Extensions.Caching.Memory.dll v7.0.</summary>
private static readonly Lazy<Func<object, IDictionary>> GetEntries7 = new(() =>
CreateGetter<object, IDictionary>(typeof(MemoryCache)
.GetNestedType("CoherentState", BindingFlags.NonPublic)
.GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance)));
//// public static IEnumerable GetKeys(this IMemoryCache memoryCache) => ((IDictionary)Collection((MemoryCache)memoryCache)).Keys;
//// public static IEnumerable<T> GetKeys<T>(this IMemoryCache memoryCache) => GetKeys(memoryCache).OfType<T>();
public static ICollection GetKeys(this IMemoryCache memoryCache) => GetEntries((MemoryCache)memoryCache).Keys;
public static IEnumerable<T> GetKeys<T>(this IMemoryCache memoryCache) => memoryCache.GetKeys().OfType<T>();
// .NET 6 and 7
private static Func<TParam, TReturn> CreateGetter<TParam, TReturn>(FieldInfo field)
{
var methodName = $"{field.ReflectedType.FullName}.get_{field.Name}";
var method = new DynamicMethod(methodName, typeof(TReturn), new[] { typeof(TParam) }, typeof(TParam), true);
var ilGen = method.GetILGenerator();
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Ldfld, field);
ilGen.Emit(OpCodes.Ret);
return (Func<TParam, TReturn>)method.CreateDelegate(typeof(Func<TParam, TReturn>));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment