5

I'm attempting to use the Microsoft.Extensions.Caching.Memory.IMemoryCache interface / class.

I need to add a new item to the cache, and make sure I don't override anything else that's already saved. At the moment, all keys are automatically generated and randomized (not sequential).

How can I test a random key for uniqueness against my current cache items?

Or, How can I get a guaranteed unique key? It'd be ok getting an auto-generated key that can be used for later retrieval.

https://learn.microsoft.com/en-us/aspnet/core/performance/caching/memory has a few examples but using the .TryGetValue(object key, object out value) for each key I generate to test uniqueness seems like overkill, and when considering multi-threaded environments this could be a problem.

The blog https://wildermuth.com/2016/04/14/Using-Cache-in-ASP-NET-Core-1-0-RC1 uses the same pattern of TryGetValue(key, out value) for deliberate Keys.

I'm hoping NOT to have to keep this list of generated keys somewhere else, i.e. another list separated list, as this would take me back to the issue of trimming the list when it grew old.

Just as an extra, in this specific case I'm be passing the keys around as url-querystring parameters, so url-compatible strings would be super. Thanks in advance.

Guid
  • 53
  • 1
  • 4
  • 1
    You mean a Guid.NewGuid()? – Filip Cordas Feb 26 '17 at 00:54
  • My understanding is that Guids are not **guaranteed** unique. My current inplementation of GetRandomKey() is Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace("=","").Replace("+","").Replace("/",""); but i'd sleep better if i could check against the current keyList. – Guid Feb 26 '17 at 02:20
  • @Guid They are. That's what *globally unique* means. – svick Feb 26 '17 at 03:06
  • Also, what's the point of all that string manipulation? Since the cache accepts `object`s as keys, why not use the `Guid` directly? – svick Feb 26 '17 at 03:07
  • in this specific case I'm passing the keys around as url-querystring parameters – Guid Feb 26 '17 at 03:33

3 Answers3

4

Using a GUID for a cache key is not a good solution, as you have already discovered. The main problem is that after the GUID is generated, there is no way to reliably regenerate it into the same key in order to get the data out of the cache.


Usually, when I create a cache the keys are based off of the entity being cached or the method that is caching it. But it is also possible to make the cache based on a combination of values that together make the value unique.

Example Using Entity

public class Employee
{
    int Id { get; set; }
    string Name { get; set; }
}

To get the key from an entity, we simply use a constant value in addition to the primary key of the entity.

private const string KEY_PREFIX = "Employee_";
private object syncLock = new object();

// innerEmployeeRetriever and cache are populated through the constructor
public Employee GetEmployee(int id)
{
    string key = KEY_PREFIX + id.ToString();

    // Get the employee from the cache
    var employee = cache[key];
    if (employee == null)
    {
        lock (syncLock)
        {
            // Double-check that another thread didn't beat us
            // to populating the cache
            var employee = cache[key];
            if (employee == null)
            {
                employee = innerEmployeeRetriever.GetEmployee(id);
                cache[key] = employee;
            }
        }
    }
    return employee;
}

Example Using Method Name

private object syncLock = new object();

// innerEmployeeRetriever and cache are populated through the constructor
public Employee GetEmployeeList()
{
    string key = "GetEmployeeList";

    // Get the employee from the cache
    var employees = cache[key];
    if (employees == null)
    {
        lock (syncLock)
        {
            // Double-check that another thread didn't beat us
            // to populating the cache
            var employees = cache[key];
            if (employees == null)
            {
                employees = innerEmployeeRetriever.GetEmployeeList();
                cache[key] = employees;
            }
        }
    }
    return employees;
}

Example Using a Combination of Values

You can also build the keys from several different values that in combination make the entity unique. This is helpful if you don't have a primary key to work with or you have several different contexts that you want to cache separately. This example was taken from MvcSiteMapProvider:

protected string GetCacheKey(string memberName)
{
    // NOTE: We must include IsReadOnly in the request cache key 
    // because we may have a different 
    // result when the sitemap is being constructed than when 
    // it is being read by the presentation layer.
    return "__MVCSITEMAPNODE_" + this.SiteMap.CacheKey + "_" + this.Key 
        + "_" + memberName + "_" + this.IsReadOnly.ToString() + "_";
}

In this case, we are building the key based on the unique key of the parent SiteMap the node belongs to, the unique key of the node, the method or property name, and whether or not the readonly flag is currently set. Each unique set of these values results in a separate cache key, making a separate cache for each combination.

Of course, for this to work right, there should be some kind of "safe" delimiter between the values to prevent the same key from being created by concatenating different values. For example, "1" + "23" is the same string as "12" + "3", but you can prevent these sort of collisions by using an underscore, pipe character, comma, or some other delimiter that won't be in the data itself to separate the values ("1" + "_" + "23" is not the same string as "12" + "_" + "3").


The bottom line is that the cache key must somehow represent what is in the cache in order for it to be useful. Your application should understand how to provide the data that makes up the key so it can be re-made if the same data needs to be retrieved again.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
0

Thanks for the comments reinforcing the Guid's Uniqueness:

I searched a little bit more on the subejct and found a post by Eric Lippert that clarified my concern: https://ericlippert.com/2012/05/07/guid-guide-part-three/

  • GUIDs are guaranteed to be unique but not guaranteed to be random. Do not use them as random numbers.

  • GUIDs that are random numbers are not cryptographic strength random numbers.

  • GUIDs are only unique when everyone cooperates; if someone wants to re-use a previously-generated GUID and thereby artificially create a collision, you cannot stop them. GUIDs are not a security mechanism.

Since I'm creating my own keys, I can trust them if I stop removing chars.

Guid
  • 53
  • 1
  • 4
0

You can always use GUID as unique key, though in theory they are of course not unique since there is a limited space for them, but in practice chances that they won't are probably less than meteorite destroying Earth. So it's rather safe to use them.

Another question is why do you want to use random id's as keys? There are cases for that of course, but usually data has some keys that you can use. E.g. if you have Users and Posts in a blog engine, you can always use $"user_{user.key}" and $"post_{post.key}" or some similar approach to have your unique keys. You can prepend with full class name as an alternative. This method will work if you have unique keys for items in the db. Since you have not fully described your situation, it's rather hard to tell if it's possible, but I'd rather try to find "natural keys" before resorting to using GUID.

P.S. Be careful with using GUID this way in the URL. It might or might not be a security problem. I.e. if you put all the data for all users in cache and allow accessing it by URL, then anyone can "guess" guid and access what they should not. It's not easy, but possible to do that since as you wrote yourself they are not guaranteed to be cryptographically random enough.

Ilya Chernomordik
  • 27,817
  • 27
  • 121
  • 207