Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.Net MemoryCache Miss when using Objects as Keys

When using IMemoryCache with an object, TryGetValue always miss. I am trying to have a tuple<string, object> as the key, and a tuple<string, string> works perfectly fine.

This code here always gets me a cache miss:

_cache.TryGetValue(("typeOfCache", query), out var something);
if(something == null) _cache.CreateEntry(("typeOfCache", query));

The object I'm using has lists of lists inside, not no dictionary/set (nothing that has a random ordering).

Is this a .net bug or am I doing something incorrectly?

like image 741
David Gourde Avatar asked Mar 05 '23 19:03

David Gourde


1 Answers

MemoryCache internally uses a ConcurrentDictionary<object, CacheEntry>, which in turn uses the default comparer for the object type, which performs equality comparisons based on the actual type's overrides of Object.Equals and Object.GetHashCode. In your case, your keys are ValueTuple<string, Query>, whatever your Query class is. ValueTuple<T1,T2>.Equals evaluates to true if the components of the compared instance are of the same types as those of the current instance, and if the components are equal to those of the current instance, with equality being determined by the default equality comparer for each component.

Thus, how the equality comparison gets performed depends on the implementation of your Query type. If this type does not override Equals and GetHashCode, nor implements IEquatable<T>, then reference equality is performed, meaning that you only get equality when you pass in the same instance of the query. If you want to alter this behavior, you should extend your Query class to implement IEquatable<Query>.

I also found that CreateEntry does not immediately add the new entry to the cache. .NET Core documentation is disappointingly sparse, so I haven't found the intended behavior; however, you can ensure that the entry is added by calling Set instead.

Example:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Caching.Memory;

class Program
{
    static void Main(string[] args)
    {
        var query1 = new Query { Parts = { new List<string> { "abc", "def", "ghi" } } };
        var query2 = new Query { Parts = { new List<string> { "abc", "def", "ghi" } } };

        var memoryCache = new MemoryCache(new MemoryCacheOptions());
        memoryCache.Set(("typeOfCache", query1), new object());
        var found = memoryCache.TryGetValue(("typeOfCache", query2), out var something);
        Console.WriteLine(found);
    }

    public class Query : IEquatable<Query>
    {
        public List<List<string>> Parts { get; } = new List<List<string>>();

        public bool Equals(Query other)
        {
            if (ReferenceEquals(this, other)) return true;
            if (ReferenceEquals(other, null)) return false;
            return this.Parts.Length == other.Parts.Length 
                && this.Parts.Zip(other.Parts, (x, y) => x.SequenceEqual(y)).All(b => b);
        }

        public override bool Equals(object obj)
        {
            return Equals(obj as Query);
        }

        public override int GetHashCode()
        {
            return this.Parts.SelectMany(p => p).Take(10).Aggregate(17, (acc, p) => acc * 23 + p?.GetHashCode() ?? 0);
        }
    }
}    
like image 116
Douglas Avatar answered Mar 17 '23 04:03

Douglas