locked
ConcurrentDictionary Keys property does not respect IEqualityComparer arg in constructor RRS feed

  • General discussion

  • When I call Contains() on the Keys property of a System.Collections.Concurrent.ConcurrentDictionary, the operation is always case sensitive, even if I construct the ConcurrentDictionary with the argument StringComparer.InvariantCultureIgnoreCase.

    This is not true of System.Collections.Generic.Dictionary.

    This seems like a bug, but I could be convinced that I misunderstood the specification.

    I am using .NET Framework 4.5.2.

    Here is code that repros the problem:

        public class Class1
        {
            private IDictionary<string, string> dictio = new ConcurrentDictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
            public void AddEntry(IDictionary<string, string> dict, string key, string value)
            {
                dict[key] = value;
            }
            public bool LookupEntry(IDictionary<string, string> dict, string key, out string value)
            {
                return dict.TryGetValue(key, out value);
            }

            public bool Contains(IDictionary<string, string> dict, string key)
            {
                return dict.Keys.Contains(key);
            }
        }

        [TestClass]
        public class UnitTest1
        {
            [TestMethod]
            public void Concurrent()
            {
                var target = new Class1();
                var dict = new ConcurrentDictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
                target.AddEntry(dict, "key", "SomeValue");
                string actualValue;
                Assert.IsTrue(target.LookupEntry(dict, "KeY", out actualValue));
                Assert.AreEqual("SomeValue", actualValue);
                Assert.IsTrue(target.Contains(dict, "KeY"));
            }
            [TestMethod]
            public void Regular()
            {
                var target = new Class1();
                var dict = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
                target.AddEntry(dict, "key", "SomeValue");
                string actualValue;
                Assert.IsTrue(target.LookupEntry(dict, "KeY", out actualValue));
                Assert.AreEqual("SomeValue", actualValue);
                Assert.IsTrue(target.Contains(dict, "KeY"));
            }
    Tuesday, September 20, 2016 8:12 PM

All replies

  • Dictionary<TKey, TValue>.Keys returns a lightweight object that just forwards the operations back to the dictionary, but ConcurrentDictionary<TKey, TValue>.Keys takes a snapshot of the current set of keys to a List<T> and then returns a ReadOnlyCollection<T> that wraps the list. ConcurrentDictionary<TKey, TValue>.Keys.Contains(TKey) thus ends up calling List<T>.Contains(T), which does not know about the comparer. It also gets linearly slower as the number of elements in the dictionary increases, so I think you should be using ConcurrentDictionary<TKey, TValue>.ContainsKey(TKey) instead, if possible.

    The documentation of ConcurrentDictionary<TKey, TValue>.Keys and IDictionary<TKey, TValue>.Keys does not say which comparer the Contains method of the returned collection uses. It could have been implemented with a dedicated class instead of ReadOnlyCollection<T> but perhaps the developers didn't think the comparer would matter. Now that the collection has been ignoring the comparer for several releases, the application compatibility risk from changing it may be greater than the benefit in consistency. If it were for me to decide, I'd just document the current behavior. Applications are already able to work around the quirk by calling Enumerable.Contains(this source, value, comparer) instead, or by copying the Keys collection to a new HashSet with the desired comparer.

    Thursday, September 22, 2016 8:29 PM