From 0c12a302300473a1049029cad36eda42895e7dd9 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 24 Mar 2026 11:34:26 +0000 Subject: [PATCH 1/7] Fixes regression in CSHARP-5942 --- src/MongoDB.Driver/FieldDefinition.cs | 25 ++- .../UpdateDefinitionBuilderTests.cs | 173 ++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) diff --git a/src/MongoDB.Driver/FieldDefinition.cs b/src/MongoDB.Driver/FieldDefinition.cs index ebdb1fd532b..e85f18668de 100644 --- a/src/MongoDB.Driver/FieldDefinition.cs +++ b/src/MongoDB.Driver/FieldDefinition.cs @@ -420,7 +420,7 @@ public static void Resolve(string fieldName, IBsonSerializer(string fieldName, IBsonSerializer(); + + Assert(subject.AddToSet("Buckets.myKey", "newValue"), "{$addToSet: {'buckets.myKey': 'newValue'}}"); + } + + [Fact] + public void AddToSet_with_expression_path_through_dictionary_to_list_value() + { + var subject = CreateSubject(); + + Assert(subject.AddToSet(x => x.Buckets["myKey"], "newValue"), "{$addToSet: {'buckets.myKey': 'newValue'}}"); + } + + [Fact] + public void AddToSetEach_with_string_path_through_dictionary_to_list_value() + { + var subject = CreateSubject(); + + Assert(subject.AddToSetEach("Buckets.myKey", new[] { "a", "b" }), "{$addToSet: {'buckets.myKey': {$each: ['a', 'b']}}}"); + } + + [Fact] + public void AddToSetEach_with_expression_path_through_dictionary_to_list_value() + { + var subject = CreateSubject(); + + Assert(subject.AddToSetEach(x => x.Buckets["myKey"], new[] { "a", "b" }), "{$addToSet: {'buckets.myKey': {$each: ['a', 'b']}}}"); + } + + [Fact] + public void AddToSet_with_string_path_through_dictionary_to_nested_list() + { + var subject = CreateSubject(); + + Assert(subject.AddToSet("Buckets.myKey.Items", "newValue"), "{$addToSet: {'buckets.myKey.items': 'newValue'}}"); + } + + [Fact] + public void AddToSet_with_expression_path_through_dictionary_to_nested_list() + { + var subject = CreateSubject(); + + Assert(subject.AddToSet(x => x.Buckets["myKey"].Items, "newValue"), "{$addToSet: {'buckets.myKey.items': 'newValue'}}"); + } + + [Fact] + public void AddToSet_with_string_path_through_dictionary_to_hashset_value() + { + var subject = CreateSubject(); + + Assert(subject.AddToSet("Buckets.myKey", "newValue"), "{$addToSet: {'buckets.myKey': 'newValue'}}"); + } + + [Fact] + public void AddToSet_with_expression_path_through_dictionary_to_hashset_value() + { + var subject = CreateSubject(); + + Assert(subject.AddToSet(x => x.Buckets["myKey"], "newValue"), "{$addToSet: {'buckets.myKey': 'newValue'}}"); + } + + [Fact] + public void AddToSet_with_string_path_through_array_rep_dictionary() + { + // When dictionary uses ArrayOfDocuments representation, string path resolution + // cannot navigate into dictionary keys via TryGetMemberSerializationInfo, + // so the field serializer falls back to null and the item serializer is + // resolved from the registry instead. + var subject = CreateSubject(); + + Assert(subject.AddToSet("Buckets.myKey", "newValue"), "{$addToSet: {'buckets.myKey': 'newValue'}}"); + } + + [Fact] + public void AddToSet_with_string_path_through_dictionary_where_value_is_IEnumerable_interface() + { + // Test with IEnumerable as dictionary value type + var subject = CreateSubject(); + + Assert(subject.AddToSet("Buckets.myKey", "newValue"), "{$addToSet: {'buckets.myKey': 'newValue'}}"); + } + + [Fact] + public void AddToSet_with_numeric_string_key_through_dictionary() + { + // When a dictionary key is all-digits (e.g., "10"), the string path resolver + // treats it as an array index rather than a dictionary member name. + // This is the exact scenario reported in the user bug: "Buckets.10" where + // Buckets is IDictionary>. + var subject = CreateSubject(); + + Assert(subject.AddToSet("Buckets.10", new DataRecord { Label = "test" }), "{$addToSet: {'buckets.10': { CreatedAt: ISODate('0001-01-01T00:00:00Z'), Label: 'test' }}}"); + } + + [Fact] + public void AddToSet_with_numeric_string_key_through_dictionary_of_List() + { + // Same scenario but with Dictionary> - numeric key "10" + // is still treated as array index by the path resolver. + var subject = CreateSubject(); + + Assert(subject.AddToSet("Buckets.10", "newValue"), "{$addToSet: {'buckets.10': 'newValue'}}"); + } + [Fact] public void BitwiseAnd() { @@ -742,5 +850,70 @@ public class F { public string Z { get; set; } } + + private class DocumentWithDictionaryOfLists + { + public ObjectId Id { get; set; } + + [BsonElement("buckets")] + public Dictionary> Buckets { get; set; } + } + + private class DocumentWithDictionaryOfObjects + { + public ObjectId Id { get; set; } + + [BsonElement("buckets")] + public Dictionary Buckets { get; set; } + } + + private class DocumentWithDictionaryOfHashSets + { + public ObjectId Id { get; set; } + + [BsonElement("buckets")] + public Dictionary> Buckets { get; set; } + } + + private class DocumentWithArrayRepDictionary + { + public ObjectId Id { get; set; } + + [BsonElement("buckets")] + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] + public Dictionary> Buckets { get; set; } + } + + private class DocumentWithDictionaryOfIEnumerable + { + public ObjectId Id { get; set; } + + [BsonElement("buckets")] + public Dictionary> Buckets { get; set; } + } + + [BsonIgnoreExtraElements] + private class DataContainer + { + public string Id { get; set; } + + [BsonElement("buckets")] + [BsonDictionaryOptions(DictionaryRepresentation.Document)] + public IDictionary> Buckets { get; set; } + = new Dictionary>(); + } + + [BsonIgnoreExtraElements] + private class DataRecord + { + public DateTime CreatedAt { get; set; } + public string Label { get; set; } + } + + private class Bucket + { + [BsonElement("items")] + public List Items { get; set; } + } } } From 6df3b7eae97a66312ee9c4aa45b6ec7ee05e92a2 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Tue, 24 Mar 2026 21:39:34 +0000 Subject: [PATCH 2/7] Go with DictionarySerializerBase fix instead. --- .../Serializers/DictionarySerializerBase.cs | 14 +++++++++++ src/MongoDB.Driver/FieldDefinition.cs | 25 +------------------ 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/MongoDB.Bson/Serialization/Serializers/DictionarySerializerBase.cs b/src/MongoDB.Bson/Serialization/Serializers/DictionarySerializerBase.cs index 5910571aa82..614e2c20f78 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/DictionarySerializerBase.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/DictionarySerializerBase.cs @@ -499,6 +499,12 @@ obj is DictionarySerializerBase other && /// public bool TryGetItemSerializationInfo(out BsonSerializationInfo serializationInfo) { + if (_dictionaryRepresentation == DictionaryRepresentation.Document && !IsNumericType(typeof(TKey))) + { + serializationInfo = null; + return false; + } + var representation = _dictionaryRepresentation == DictionaryRepresentation.ArrayOfArrays ? BsonType.Array : BsonType.Document; @@ -510,6 +516,14 @@ public bool TryGetItemSerializationInfo(out BsonSerializationInfo serializationI return true; } + private static bool IsNumericType(Type type) + { + return type == typeof(int) || type == typeof(long) || + type == typeof(short) || type == typeof(byte) || + type == typeof(uint) || type == typeof(ulong) || + type == typeof(ushort) || type == typeof(sbyte); + } + /// public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo serializationInfo) { diff --git a/src/MongoDB.Driver/FieldDefinition.cs b/src/MongoDB.Driver/FieldDefinition.cs index e85f18668de..ebdb1fd532b 100644 --- a/src/MongoDB.Driver/FieldDefinition.cs +++ b/src/MongoDB.Driver/FieldDefinition.cs @@ -420,7 +420,7 @@ public static void Resolve(string fieldName, IBsonSerializer(string fieldName, IBsonSerializer Date: Tue, 24 Mar 2026 23:07:40 +0000 Subject: [PATCH 3/7] Clarify which bug the test is referring to. --- tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs index eda39df5add..f50800c7ede 100644 --- a/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs @@ -157,7 +157,7 @@ public void AddToSet_with_numeric_string_key_through_dictionary() { // When a dictionary key is all-digits (e.g., "10"), the string path resolver // treats it as an array index rather than a dictionary member name. - // This is the exact scenario reported in the user bug: "Buckets.10" where + // This is the exact scenario reported in CSHARP-5924 - "Buckets.10" where // Buckets is IDictionary>. var subject = CreateSubject(); From 6cef8c6f304ca3eb7c91e431b7700bca50f6d245 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Thu, 26 Mar 2026 15:58:57 +0000 Subject: [PATCH 4/7] Feedback from PR --- .../SerializerFinderHelperMethods.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderHelperMethods.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderHelperMethods.cs index 7ca8e8e5449..8ae0066575e 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderHelperMethods.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderHelperMethods.cs @@ -17,7 +17,9 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Options; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.Linq.Linq3Implementation.Misc; using MongoDB.Driver.Linq.Linq3Implementation.Serializers; @@ -81,7 +83,21 @@ IBsonSerializer CreateCollectionSerializerFromCollectionSerializer(Type collecti return UnknowableSerializer.Create(collectionType); } - var itemSerializer = collectionSerializer.GetItemSerializer(); + IBsonSerializer itemSerializer; + if (collectionSerializer is IBsonDictionarySerializer dictionarySerializer && + collectionSerializer is IBsonArraySerializer arraySerializer && + !arraySerializer.TryGetItemSerializationInfo(out _)) + { + var representation = dictionarySerializer.DictionaryRepresentation == DictionaryRepresentation.ArrayOfArrays + ? BsonType.Array + : BsonType.Document; + itemSerializer = KeyValuePairSerializer.Create(representation, dictionarySerializer.KeySerializer, dictionarySerializer.ValueSerializer); + } + else + { + itemSerializer = collectionSerializer.GetItemSerializer(); + } + return CreateCollectionSerializerFromItemSerializer(collectionType, itemSerializer); } From 371c3889c0c586de91fa41044bf9e4b46091f914 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Fri, 27 Mar 2026 11:11:11 +0000 Subject: [PATCH 5/7] Fix select many path/test --- .../SerializerFinderHelperMethods.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderHelperMethods.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderHelperMethods.cs index 8ae0066575e..06f0374bf77 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderHelperMethods.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderHelperMethods.cs @@ -251,11 +251,22 @@ private void DeduceUnknowableSerializer(Expression node) private bool IsItemSerializerKnown(Expression node, out IBsonSerializer itemSerializer) { if (IsKnown(node, out var nodeSerializer) && - nodeSerializer is IBsonArraySerializer arraySerializer && - arraySerializer.TryGetItemSerializationInfo(out var itemSerializationInfo)) + nodeSerializer is IBsonArraySerializer arraySerializer) { - itemSerializer = itemSerializationInfo.Serializer; - return true; + if (arraySerializer.TryGetItemSerializationInfo(out var itemSerializationInfo)) + { + itemSerializer = itemSerializationInfo.Serializer; + return true; + } + + if (nodeSerializer is IBsonDictionarySerializer dictionarySerializer) + { + var representation = dictionarySerializer.DictionaryRepresentation == DictionaryRepresentation.ArrayOfArrays + ? BsonType.Array + : BsonType.Document; + itemSerializer = KeyValuePairSerializer.Create(representation, dictionarySerializer.KeySerializer, dictionarySerializer.ValueSerializer); + return true; + } } itemSerializer = null; From 75defcec22a5917c5ff562ad88f0ee17caaf9fb3 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Mon, 13 Apr 2026 15:33:58 +0100 Subject: [PATCH 6/7] Remove confusing test comments --- tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs index f50800c7ede..a109db3ff47 100644 --- a/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs @@ -155,10 +155,6 @@ public void AddToSet_with_string_path_through_dictionary_where_value_is_IEnumera [Fact] public void AddToSet_with_numeric_string_key_through_dictionary() { - // When a dictionary key is all-digits (e.g., "10"), the string path resolver - // treats it as an array index rather than a dictionary member name. - // This is the exact scenario reported in CSHARP-5924 - "Buckets.10" where - // Buckets is IDictionary>. var subject = CreateSubject(); Assert(subject.AddToSet("Buckets.10", new DataRecord { Label = "test" }), "{$addToSet: {'buckets.10': { CreatedAt: ISODate('0001-01-01T00:00:00Z'), Label: 'test' }}}"); @@ -167,8 +163,6 @@ public void AddToSet_with_numeric_string_key_through_dictionary() [Fact] public void AddToSet_with_numeric_string_key_through_dictionary_of_List() { - // Same scenario but with Dictionary> - numeric key "10" - // is still treated as array index by the path resolver. var subject = CreateSubject(); Assert(subject.AddToSet("Buckets.10", "newValue"), "{$addToSet: {'buckets.10': 'newValue'}}"); From 8796a2f35a7f8fcbc600a2d7ac4743c3f946dcf3 Mon Sep 17 00:00:00 2001 From: Damien Guard Date: Wed, 15 Apr 2026 16:23:29 +0100 Subject: [PATCH 7/7] Remove inaccurate comments from tests. --- tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs b/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs index a109db3ff47..403e313b575 100644 --- a/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs +++ b/tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs @@ -134,10 +134,6 @@ public void AddToSet_with_expression_path_through_dictionary_to_hashset_value() [Fact] public void AddToSet_with_string_path_through_array_rep_dictionary() { - // When dictionary uses ArrayOfDocuments representation, string path resolution - // cannot navigate into dictionary keys via TryGetMemberSerializationInfo, - // so the field serializer falls back to null and the item serializer is - // resolved from the registry instead. var subject = CreateSubject(); Assert(subject.AddToSet("Buckets.myKey", "newValue"), "{$addToSet: {'buckets.myKey': 'newValue'}}"); @@ -146,7 +142,6 @@ public void AddToSet_with_string_path_through_array_rep_dictionary() [Fact] public void AddToSet_with_string_path_through_dictionary_where_value_is_IEnumerable_interface() { - // Test with IEnumerable as dictionary value type var subject = CreateSubject(); Assert(subject.AddToSet("Buckets.myKey", "newValue"), "{$addToSet: {'buckets.myKey': 'newValue'}}");