Last active
August 29, 2023 20:20
-
-
Save DamianSuess/55aa7d94cfa6074fa68c80484121c2ba to your computer and use it in GitHub Desktop.
C# CacheService
This file contains 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; | |
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>(); | |
} |
This file contains 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 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); | |
} | |
} |
This file contains 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; | |
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>(); | |
} |
This file contains 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; | |
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