How to inspect cache policies inside System.Runtime.Caching.ObjectCache?

12.1k views Asked by At

I'm making use of the new .NET 4.0 Caching namespace: System.Runtime.Caching.

Now, i'm just doing some prototype/fiddling with the new API, in order to work out the best fit for the real app.

In line with that, i'm trying to create a page (ASP.NET MVC) that basically dumps out everything in the cache, particularly the following info:

  • Cache Key
  • Cache Object
  • Cache Policy (expiry date, etc)
  • Cache Dependencies (if any)

However, i can't seem to get anything except the key/object.

Here's the code i'm currently playing with:

public ActionResult Index()
{
   var cache = MemoryCache.Default;

   // i can get the list of cache keys like this:
   var cacheKeys = cache.Select(kvp => kvp.Key).ToList();

   // i can also get a strongly-typed "CacheItem" like this:
   CacheItem item = cache.GetCacheItem("someKey");

}

I would have hoped the "CacheItem" class would expose the information i require (expiry, dependencies, etc - at least as "getters").

But it doesn't. All it has is properties for key, value and region name.

How can i inspect the items in the cache and spit out the information i require?

Is there a namespace/class i'm missing?

EDIT

Looks like there is a ChangeMonitor class, but again - this doesn't give expiration info, it just allows you to subscribe to events when cache items are removed.

There must be a way to just grab the items in the cache, and when they expire.

EDIT 2

Don't know if this should be a seperate question, but also - i'm confused as to what lifetime i should give my ObjectCache. MSDN says it's not a singleton, and you can in fact create multiple ObjectCache instances. What does that mean though, i have to use a fully-locked singleton when accessing the ObjectCache instance?

2

There are 2 answers

1
Lee Dale On BEST ANSWER

It doesn't look to me that there is a way to retrieve the CacheItemPolicy once it's been added to the cache collection.

The best way around this is can think of is to cache the policy object along with the item you want to cache but just appending "Policy" to the key name so that you can later retrieve the policy. This obviously assumes you have control over actually adding the item to the cache in the first place. Example below:

public ActionResult Index()
    {
        string key = "Hello";
        string value = "World";

        var cache = MemoryCache.Default;
        CacheItemPolicy policy = new CacheItemPolicy();
        policy.AbsoluteExpiration = DateTime.Now.AddDays(1);
        cache.Add(new CacheItem(key, value), policy);
        cache.Add(new CacheItem(key + "Policy", policy), null);

        CacheItem item = cache.GetCacheItem(key);
        CacheItem policyItem = cache.GetCacheItem(key + "Policy");
        CacheItemPolicy policy2 = policyItem.Value as CacheItemPolicy;

        ViewBag.Message = key + " " + value;

        return View();
    }
1
Fernando Ferreira On

I recently had to create unit tests for a MemoryCache wrapper. I wanted to check if the wrapper was adding to the underlying cache using the expected policies.

I came up with the following GetCacheItemPolicy extension method which heavily uses reflection. Even though my purpose was solely unit testing my wrapper, I tried to optimize every reflection so that it is usable in other contexts where performance matters (even though that if you're doing this in production code, most likely performance does not matter, but hey...).

internal static class MemoryCacheExtensions
{
    static MemoryCacheExtensions()
    {
        const BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;

        Expression<Func<Func<object, object>>> createGetterExpression = () => CreateObjectGetter<object, object>(null);
        var genericCreateObjectGetterMethodInfo = ((MethodCallExpression)createGetterExpression.Body).Method.GetGenericMethodDefinition();

        // Basic types
        var memoryCacheType = typeof(MemoryCache);
        var getEntryMethodInfo = memoryCacheType.GetMethod("GetEntry", flags);

        // MemoryCacheEntry fields
        Debug.Assert(getEntryMethodInfo != null, nameof(getEntryMethodInfo) + " != null");
        var memoryCacheEntryType = getEntryMethodInfo.ReturnType;
        var valueFieldInfo = memoryCacheEntryType.GetField("_value", flags);
        var usageBucketFieldInfo = memoryCacheEntryType.GetField("_usageBucket", flags);
        var removedCallbackFieldInfo = memoryCacheEntryType.GetField("_callback", flags);
        var seldomUsedFieldsFieldInfo = memoryCacheEntryType.GetField("_fields", flags);
        var slidingExpirationFieldInfo = memoryCacheEntryType.GetField("_slidingExp", flags);
        var absoluteExpirationFieldInfo = memoryCacheEntryType.GetField("_utcAbsExp", flags);

        // SeldomUsedFields fields
        Debug.Assert(seldomUsedFieldsFieldInfo != null, nameof(seldomUsedFieldsFieldInfo) + " != null");
        var seldomUsedFieldsType = seldomUsedFieldsFieldInfo.FieldType;
        var dependenciesFieldInfo = seldomUsedFieldsType.GetField("_dependencies", flags);

        // SentinelEntry fields
        var sentinelEntryType = memoryCacheType.GetNestedType("SentinelEntry", flags);
        var updateCallbackFieldInfo = sentinelEntryType.GetField("_updateCallback", flags);

        GetMemoryCacheEntry = (Func<MemoryCache, string, object>)Delegate
            .CreateDelegate(typeof(Func<MemoryCache, string, object>), getEntryMethodInfo);

        Debug.Assert(valueFieldInfo != null, nameof(valueFieldInfo) + " != null");
        GetMemoryCacheEntryValue = (Func<object, object>)genericCreateObjectGetterMethodInfo
            .MakeGenericMethod(memoryCacheEntryType, valueFieldInfo.FieldType)
            .Invoke(null, new object[] { valueFieldInfo });

        Debug.Assert(usageBucketFieldInfo != null, nameof(usageBucketFieldInfo) + " != null");
        GetMemoryCacheEntryUsageBucket = (Func<object, byte>)genericCreateObjectGetterMethodInfo
            .MakeGenericMethod(memoryCacheEntryType, usageBucketFieldInfo.FieldType)
            .Invoke(null, new object[] { usageBucketFieldInfo });

        Debug.Assert(removedCallbackFieldInfo != null, nameof(removedCallbackFieldInfo) + " != null");
        GetMemoryCacheEntryRemovedCallback = (Func<object, CacheEntryRemovedCallback>)genericCreateObjectGetterMethodInfo
            .MakeGenericMethod(memoryCacheEntryType, removedCallbackFieldInfo.FieldType)
            .Invoke(null, new object[] { removedCallbackFieldInfo });

        GetMemoryCacheEntrySeldomUsedFields = (Func<object, object>)genericCreateObjectGetterMethodInfo
            .MakeGenericMethod(memoryCacheEntryType, seldomUsedFieldsFieldInfo.FieldType)
            .Invoke(null, new object[] { seldomUsedFieldsFieldInfo });

        Debug.Assert(slidingExpirationFieldInfo != null, nameof(slidingExpirationFieldInfo) + " != null");
        GetMemoryCacheEntrySlidingExpiration = (Func<object, TimeSpan>)genericCreateObjectGetterMethodInfo
            .MakeGenericMethod(memoryCacheEntryType, slidingExpirationFieldInfo.FieldType)
            .Invoke(null, new object[] { slidingExpirationFieldInfo });

        Debug.Assert(absoluteExpirationFieldInfo != null, nameof(absoluteExpirationFieldInfo) + " != null");
        GetMemoryCacheEntryAbsoluteExpiration = (Func<object, DateTime>)genericCreateObjectGetterMethodInfo
            .MakeGenericMethod(memoryCacheEntryType, absoluteExpirationFieldInfo.FieldType)
            .Invoke(null, new object[] { absoluteExpirationFieldInfo });

        Debug.Assert(dependenciesFieldInfo != null, nameof(dependenciesFieldInfo) + " != null");
        GetSeldomUsedFieldsDependencies = (Func<object, Collection<ChangeMonitor>>)genericCreateObjectGetterMethodInfo
            .MakeGenericMethod(seldomUsedFieldsType, dependenciesFieldInfo.FieldType)
            .Invoke(null, new object[] { dependenciesFieldInfo });

        Debug.Assert(updateCallbackFieldInfo != null, nameof(updateCallbackFieldInfo) + " != null");
        GetSentinelEntryUpdateCallback = (Func<object, CacheEntryUpdateCallback>)genericCreateObjectGetterMethodInfo
            .MakeGenericMethod(sentinelEntryType, updateCallbackFieldInfo.FieldType)
            .Invoke(null, new object[] { updateCallbackFieldInfo });
    }

    private static Func<MemoryCache, string, object> GetMemoryCacheEntry { get; }

    private static Func<object, object> GetMemoryCacheEntryValue { get; }
    private static Func<object, byte> GetMemoryCacheEntryUsageBucket { get; }
    private static Func<object, CacheEntryRemovedCallback> GetMemoryCacheEntryRemovedCallback { get; }
    private static Func<object, object> GetMemoryCacheEntrySeldomUsedFields { get; }
    private static Func<object, TimeSpan> GetMemoryCacheEntrySlidingExpiration { get; }
    private static Func<object, DateTime> GetMemoryCacheEntryAbsoluteExpiration { get; }

    private static Func<object, Collection<ChangeMonitor>> GetSeldomUsedFieldsDependencies { get; }

    private static Func<object, CacheEntryUpdateCallback> GetSentinelEntryUpdateCallback { get; }

    public static CacheItemPolicy GetCacheItemPolicy(this MemoryCache memoryCache, string key)
    {
        var entry = GetMemoryCacheEntry(memoryCache, key);
        if (entry == null) return null;

        var sentinel = GetMemoryCacheEntry(memoryCache, "OnUpdateSentinel" + key);
        var sentinelValue = sentinel == null ? null : GetMemoryCacheEntryValue(sentinel);

        var usageBucket = GetMemoryCacheEntryUsageBucket(entry);

        var slidingExpiration = GetMemoryCacheEntrySlidingExpiration(entry);
        var absoluteExpiration = GetMemoryCacheEntryAbsoluteExpiration(entry);

        var seldomUsedFields = GetMemoryCacheEntrySeldomUsedFields(entry);
        var changeMonitors = seldomUsedFields == null ? null : GetSeldomUsedFieldsDependencies(seldomUsedFields);

        var removedCallback = GetMemoryCacheEntryRemovedCallback(entry);
        var updatedCallback = sentinelValue == null ? null : GetSentinelEntryUpdateCallback(sentinelValue);

        var cacheItemPolicy = new CacheItemPolicy
        {
            Priority = usageBucket == 0xFF ? CacheItemPriority.NotRemovable : CacheItemPriority.Default,
            AbsoluteExpiration = absoluteExpiration,
            RemovedCallback = removedCallback,
            SlidingExpiration = slidingExpiration,
            UpdateCallback = updatedCallback,
        };

        if (changeMonitors != null)
        {
            foreach (var changeMonitor in changeMonitors)
            {
                cacheItemPolicy.ChangeMonitors.Add(changeMonitor);
            }
        }

        return cacheItemPolicy;
    }

    private static Func<TTarget, TFieldType> CreateGetter<TTarget, TFieldType>(FieldInfo field)
    {
        var fieldReflectedType = field.ReflectedType;
        Debug.Assert(fieldReflectedType != null, "field.ReflectedType != null");

        var methodName = fieldReflectedType.FullName + ".get_" + field.Name;
        var getterMethod = new DynamicMethod(methodName, typeof(TFieldType), new[] { typeof(TTarget) }, true);
        var generator = getterMethod.GetILGenerator();

        if (field.IsStatic)
        {
            generator.Emit(OpCodes.Ldsfld, field);
        }
        else
        {
            generator.Emit(OpCodes.Ldarg_0);
            generator.Emit(OpCodes.Ldfld, field);
        }

        generator.Emit(OpCodes.Ret);

        return (Func<TTarget, TFieldType>)getterMethod.CreateDelegate(typeof(Func<TTarget, TFieldType>));
    }

    private static Func<object, TFieldType> CreateObjectGetter<TTarget, TFieldType>(FieldInfo fieldInfo)
    {
        var getter = CreateGetter<TTarget, TFieldType>(fieldInfo);
        return @object => getter((TTarget)@object);
    }
}