Jozef Cechovsky Jozef Cechovsky - 1 month ago 6
C# Question

ConcurrentDictionary.GetOrAdd - Add only if not null

I'm using ConcurrentDictionary to cache data with parallel access and sometimes new items can be stored in db and they are not loaded into cache. This is reason why I use GetOrAdd

public User GetUser(int userId)
{
return _user.GetOrAdd(userId, GetUserFromDb);
}

private User GetUserFromDb(int userId)
{
var user = _unitOfWork.UserRepository.GetById(userId);

// if user is null, it is stored to dictionary

return user;
}


But how I can check if user was get from db and store user to dictionary only if user is not null?

Possibly I can remove null from ConcurrentDictionary immediately after GetOrAdd but it doesn't look thread safe and it is not very elegant solution. Useless insert and remove from dictionary. Do you have any idea how to do it?

Answer
public User GetUser(int userId)
{
    var user = _user.GetOrAdd(userId, GetUserFromDb);
    if (user == null) _user.TryRemove(userId, out user);    
}

You can also wrap that into an extension method:

public static TValue GetOrAddIfNotNull<TKey, TValue>(
    this ConcurrentDictionary<TKey, TValue> dictionary,
    TKey key, 
    Func<TKey, TValue> valueFactory) where TValue : class
{
    var value = dictionary.GetOrAdd(key, valueFactory);
    if (value == null) dictionary.TryRemove(key, out value);
    return value;
}

Then your code will look like:

public User GetUser(int userId)
{
    var user = _user.GetOrAddIfNotNull(userId, GetUserFromDb)   
}

UPDATE

As per @usr comment, there might be a case when:

  1. Thread 1 executes GetOrAdd, adds null to the dictionary and pauses.
  2. User is added to the database.
  3. Thread 2 executes GetOrAdd and retrieves null from the dictionary instead of hitting the database.
  4. Thread 1 and Thread 2 execute TryRemove and remove record from the dictionary.

With this timing, Thread 2 will get null instead of hitting the database and getting the user record. If this edge case matters to you and you still want to use ConcurrentDictionary, then you can use lock in the extension method:

public static class ConcurrentDictionaryExtensions
{
    private static readonly object myLock = new object();

    public static TValue GetOrAddIfNotNull<TKey, TValue>(
        this ConcurrentDictionary<TKey, TValue> dictionary,
        TKey key, 
        Func<TKey, TValue> valueFactory) where TValue : class
    {
        lock (myLock)
        {
            var value = dictionary.GetOrAdd(key, valueFactory);
            if (value == null) dictionary.TryRemove(key, out value);
            return value;
        }
    }
}