.NET doesn’t expose a RemoveIf
directly, but it does expose the building blocks necessary to make it work without doing your own locking.
ConcurrentDictionary
implements ICollection<T>
, which has a Remove
that takes and tests for a full KeyValuePair
instead of just a key. Despite being hidden, this Remove
is still thread-safe and we’ll use it to implement this. One caveat for this to work is that Remove
uses EqualityComparer<T>.Default
to test the value, so it must be equality comparable. Your current one is not, so we’ll re-implement that as such:
struct ObjectCount : IEquatable<ObjectCount>
{
public object Object { get; }
public int Count { get; }
public ObjectCount(object o, int c)
{
Object = o;
Count = c;
}
public bool Equals(ObjectCount o) =>
object.Equals(Object, o.Object) && Count == o.Count;
public override bool Equals(object o) =>
(o as ObjectCount?)?.Equals(this) == true;
// this hash combining will work but you can do better.
// it is not actually used by any of this code.
public override int GetHashCode() =>
(Object?.GetHashCode() ?? 0) ^ Count.GetHashCode();
}
And finally, we’ll define a method to increment/decrement counts from your dictionary:
void UpdateCounts(ConcurrentDictionary<string, ObjectCount> dict, string key, int toAdd)
{
var addOrUpdateValue = dict.AddOrUpdate(key,
new ObjectCount(new object(), 1),
(k, pair) => new ObjectCount(pair.Key, pair.Value + toAdd));
if(addOrUpdateValue.Count == 0)
{
((ICollection<KeyValuePair<string, ObjectCount>>)dict).Remove(
new KeyValuePair<string, ObjectCount>(key, addOrUpdateValue));
}
}
The value for that key might be changed between the calls of AddOrUpdate
and Remove
, but that doesn’t matter to us: because Remove
tests the full KeyValuePair
, it will only remove it if the value hasn’t changed since the update.
This is the common lock-free pattern of setting up a change and then using a final thread-safe op to safely “commit” the change only if our data structure hasn’t been updated in the meantime.