6

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?

David Gourde
  • 3,709
  • 2
  • 31
  • 65

1 Answers1

10

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);
        }
    }
}    
Douglas
  • 53,759
  • 13
  • 140
  • 188
  • Ahhh, it makes sense now. I’ll make sure the objects override Equals or simply serialize them. Thanks a lot. – David Gourde Jan 06 '19 at 07:19
  • 1
    Welcome. Don't forget to override `GetHashCode` too; dictionaries use it for lookups. Two objects that are equal must return the same hash. – Douglas Jan 06 '19 at 08:53
  • @Douglas - Any chance you could demystify what you're doing in GetHashCode with all of that LINQ, and how it ensures two instances of Query will generate the same hash code if their Parts have value equality? – bubbleking Apr 13 '21 at 16:46
  • @bubbleking: The `GetHashCode` method combines the hash codes of the first ten elements from the flattened `Parts` list. The `Aggregate` operator uses the popular pattern for combining multiple hash codes using primes; see [this answer](https://stackoverflow.com/a/263416/1149773). – Douglas Apr 13 '21 at 18:29
  • So then, why 10? Won't that lead to the same hashcode being generated for two collections with 11+ elements even if the 11th (or higher) elements in them are not the same? – bubbleking Apr 14 '21 at 14:20
  • @bubbleking: Correct. Hash codes aren't expected to be unique, just well distributed. Importantly, hash codes need to be computed fast, which leads to this trade-off of considering just 10 elements. If distribution is more important than performance in your scenario, you can remove that `Take(10)`. – Douglas Apr 14 '21 at 16:34