简体   繁体   中英

How to cache an object with multiple keys in C#

I have models that are unique in 2 or more properties. For example, objects of the class Entity are unique both by name and by ID.

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

And I have a repository for the model:

public class EntityRepository
{
    ...
    public Entity GetById(int id)
    {
        return db.GetById(id);
    }
    public Entity GetByName(string name)
    {
        return db.GetByName(name);
    }
}

What is the best way to cache both calls to GetById and calls to GetByName using Microsoft.Extensions.Caching.Memory.IMemoryCache ?

The current solution:

public class EntityRepository
{
    ...
    public Entity GetById(int id)
    {
        return Cache.GetOrCreate($"id:{id}", cacheEntry =>
        {
            return db.GetById(id);
        });
    }
    public Entity GetByName(string name)
    {
        return Cache.GetOrCreate($"name:{name}", cacheEntry =>
        {
            return db.GetByName(name);
        });
    }
    public void RemoveById(int id)
    {
        db.RemoveById(id);
        Cache.Remove($"id:{id}");
    }
}

The problem here is that if I delete an entity by its ID, I would be able to remove it from the cache via ID, but it would still exist with the other key. And there is a similar problem for updating entities.

Is there a better solution than saving the object twice in the cache?

In your case I would cache in a different way, basically, I would keep the cached entries in a list in my cache, and retrieve the list and search there.

If you think the list will get too big to search into, then you might want to partition a bit for performance reasons.

A general example would be the following.

public class EntityRepository
{

    public Entity GetById(int id)
    {
        List<Entity> entities = Cache.GetOrCreate($"entities", cacheEntry =>
        {
            // Create an empty list of entities
            return new List<Entity>();
        });

        // Look for the entity
        var entity = entities.Where(e => e.id == id).FirstOrDefault();
        // if not there, then add it to the cached list
        if (entity == null)
        {
            entity = db.GetById(id);
            entities.Add(entity)
            // Update the cache
            Cache.Set($"entities", entities);
        }
        return entity;
    }
    public Entity GetByName(string name)
    {
        // Same thing with id    
    }
    public void RemoveById(int id)
    {
        // load the list, remove item and update cache
    }
}

In any other case, you need to wrap around your implementation, with some kind of logic. Maybe multi key dictionaries, or some kind of datastructure to retain history and do custom cleanups. There is nothing out of the box though.

You can also simplify code and repeating, by extracting the list of entities into a getter and setter like this:

public List<Entity> Entities
{
    get { return Cache.GetOrCreate($"entities", cacheEntry =>
                 {
                     // Create an empty list of entities
                     return new List<Entity>();
                 }); 
    }
    set { Cache.Set($"entities", value); }
}

Memory Cache stores cache items as key-value pairs, so we can take this to our advantage.

so instead of doing this :

public Entity GetById(int id)
{
    return Cache.GetOrCreate($"id:{id}", cacheEntry =>
    {
        return db.GetById(id);
    });
}

you could do something like this :

public Entity GetById(int id)
{
    string _name = string.Empty;

    if (!_cache.TryGetValue(id, out _name))
    {
        var _entity = db.GetById(id);

        _cache.Set(_entity.Id, _entity.Name);
    }

    return _cache.Get<Entity>(id);
}

on get, it'll check the cache if not exists, then it'll get the values from the source and store it in the cache, then it'll return the cache.

now since you need also to check the values and not the keys on the GetByName method, you could cast the cache to a dictionary, and use Linq to retrieve the key by its value.

    public Entity GetByName(string name)
    {
        int id;

        var dic = _cache as IDictionary<int, string>;

        if (!dic.Values.Contains(name))
        {
            var _entity = db.GetByName(name);

            _cache.Set(_entity.Id, _entity.Name);

            id = _entity.Id;
        }
        else
        {
            id = dic.FirstOrDefault(x => x.Value == name).Key;
        }

        return _cache.Get<Entity>(id);
    }

now when you use RemoveById you just pass the id :

public void RemoveById(int id)
{
    db.RemoveById(id);
    _cache.Remove(id);
}

I have not tested the above code, I just wanted to give you some insights that could lead you to your desired results.

Not sure if there is a better solution than storing object twice in cache but there is a simple solution for deleting/updating. First, get the an object by Id and then remove all its cache entries by known properties values

public class EntityRepository
{
    public Entity GetById(int id)
    {
        return Cache.GetOrCreate($"id:{id}", cacheEntry =>
        {
            return db.GetById(id);
        });
    }
    public Entity GetByName(string name)
    {
        return Cache.GetOrCreate($"name:{name}", cacheEntry =>
        {
            return db.GetByName(name);
        });
    }
    public void RemoveById(int id)
    {
        db.RemoveById(id);
        if (Cache.TryGetValue(id, out Entity entity))
        {
            Cache.Remove($"id:{entity.Id}");
            Cache.Remove($"name:{entity.Name}");
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM