MonkeyCoder
MonkeyCoder

Reputation: 2620

Cache key construction based on the method name and argument values

I've decided to implement a caching facade in one of our applications - the purpose is to eventually reduce the network overhead and limit the amount of db hits. We are using Castle.Windsor as our IoC Container and we have decided to go with Interceptors to add the caching functionality on top of our services layer using the System.Runtime.Caching namespace.

At this moment I can't exactly figure out what's the best approach for constructing the cache key. The goal is to make a distinction between different methods and also include passed argument values - meaning that these two method calls should be cached under two different keys:

IEnumerable<MyObject> GetMyObjectByParam(56); // key1
IEnumerable<MyObject> GetMyObjectByParam(23); // key2

For now I can see two possible implementations:

Option 1: assembly | class | method return type | method name | argument types | argument hash codes

"MyAssembly.MyClass IEnumerable<MyObject> GetMyObjectByParam(long) { 56 }";

Option 2: MD5 or SHA-256 computed hash based on the method's fully-qualified name and passed argument values

string key = new SHA256Managed().ComputeHash(name + args).ToString();

I'm thinking about the first option as the second one requires more processing time - on the other hand the second option enforces exactly the same 'length' of all generated keys.

Is it safe to assume that the first option will generate a unique key for methods using complex argument types? Or maybe there is a completely different way of doing this?

Help and opinion will by highly appreciated!

Upvotes: 11

Views: 5689

Answers (2)

MonkeyCoder
MonkeyCoder

Reputation: 2620

Based on some very useful links that I've found here and here I've decided to implement it more-or-less like this:

public sealed class CacheKey : IEquatable<CacheKey>
{
    private readonly Type reflectedType;
    private readonly Type returnType;
    private readonly string name;
    private readonly Type[] parameterTypes;
    private readonly object[] arguments;

    public User(Type reflectedType, Type returnType, string name, 
        Type[] parameterTypes, object[] arguments)
    {
        // check for null, incorrect values etc.

        this.reflectedType = reflectedType;
        this.returnType = returnType;
        this.name = name;
        this.parameterTypes = parameterTypes;
        this.arguments = arguments;
    }

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

    public bool Equals(CacheKey other)
    {
        if (other == null)
        {
            return false;
        }

        for (int i = 0; i < parameterTypes.Count; i++)
        {
            if (!parameterTypes[i].Equals(other.parameterTypes[i]))
            {
                return false;
            }
        }

        for (int i = 0; i < arguments.Count; i++)
        {
            if (!arguments[i].Equals(other.arguments[i]))
            {
                return false;
            }
        }

        return reflectedType.Equals(other.reflectedType) &&
           returnType.Equals(other.returnType) &&
           name.Equals(other.name);
    }

    private override int GetHashCode()
    {
        unchecked
        {
            int hash = 17;

            hash = hash * 31 + reflectedType.GetHashCode();
            hash = hash * 31 + returnType.GetHashCode();
            hash = hash * 31 + name.GetHashCode();

            for (int i = 0; i < parameterTypes.Count; i++)
            {
                hash = hash * 31 + parameterTypes[i].GetHashCode();
            }

            for (int i = 0; i < arguments.Count; i++)
            {
                hash = hash * 31 + arguments[i].GetHashCode();
            }

            return hash;
        }
    }
}

Basically it's just a general idea - the above code can be easily rewritten to a more generic version with one collection of Fields - the same rules would have to be applied on each element of the collection. I can share the full code.

Upvotes: 5

Patrick M
Patrick M

Reputation: 10989

An option you seem to have skipped is using the .NET built in GetHashCode() function for the string. I'm fairly certain this is what would go on behind the scenes in a C# dictionary with a String as the <TKey> (I mention that because you've tagged the question with dictionary). I'm not sure how the .NET dictionary class relates to your Castle.Windsor or the system.runtime.caching interface you mention.

The reason you wouldn't want to use GetHashCode as a hash key is that the functionality is specifically disclaimed by MicroSoft to change between versions without warning (as in to provide a more unique or faster executing function). If this cache will live strictly in memory, then this is not a concern because upgrading the .NET framework would necessitate a restart of your application, wiping the cache.

To clarify, just using the concatenated string (Option 1) should be sufficiently unique. It looks like you've added everything possible to uniquely qualify your methods.

If you end up feeding the String of an MD5 or Sha256 into a dictionary key, the program would probably rehash the string behind the scenes anyways. It's been a while since I read about the inner workings of the Dictionary class. If you leave it as a Dictionary<String, IEnumerable<MyObject>> (as opposed to calling GetHashCode() on the strings yourself using the int return value as the key) then the dictionary should handle collisions of the hash code itself.

Also note that (at least according to a benchmark program run on my machine), MD5 is around 10% faster than SHA1 and twice as fast as SHA256. String.GetHashCode() is around 20 times faster than MD5 (it's not cryptographically secure). Tests were taken for the total time to compute the hashes for the same 100,000 randomly generated strings of length between 32 and 1024 characters. But regardless of the exact numbers, using a cryptographically secure hash function as a key will only slow down your program.

I can post the source code for my comparisons if you like.

Upvotes: 2

Related Questions