From 02f691a17272398c7a22b891db7ed975ba0412bc Mon Sep 17 00:00:00 2001 From: rstam Date: Fri, 7 Nov 2025 18:50:42 -0500 Subject: [PATCH 1/5] CSHARP-5779: Support Dictionary Keys and Values properties --- .../Ast/Expressions/AstExpression.cs | 5 + .../DictionaryKeyCollectionSerializer.cs | 49 ++ .../Serializers/DictionarySerializer.cs | 52 +++ .../DictionaryValueCollectionSerializer.cs | 57 +++ .../Serializers/ICollectionSerializer.cs | 48 ++ .../KeyValuePairWrappedValueSerializer.cs | 54 +++ ...essionToAggregationExpressionTranslator.cs | 22 +- ...essionToAggregationExpressionTranslator.cs | 75 ++++ .../Jira/CSharp5779Tests.cs | 420 ++++++++++++++++++ 9 files changed, 779 insertions(+), 3 deletions(-) create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryKeyCollectionSerializer.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionarySerializer.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryValueCollectionSerializer.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/ICollectionSerializer.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/KeyValuePairWrappedValueSerializer.cs create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs index bf729df32a9..192423803fe 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs @@ -224,6 +224,11 @@ public static AstExpression ComputedDocument(IEnumerable field return new AstComputedDocumentExpression(fields); } + public static AstExpression ComputedDocument(IEnumerable<(string Name, AstExpression Value)> fields) + { + return new AstComputedDocumentExpression(fields.Select(f => AstExpression.ComputedField(f.Name, f.Value))); + } + public static AstComputedField ComputedField(string name, AstExpression value) { return new AstComputedField(name, value); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryKeyCollectionSerializer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryKeyCollectionSerializer.cs new file mode 100644 index 00000000000..836262a6bd8 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryKeyCollectionSerializer.cs @@ -0,0 +1,49 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Serializers; + +internal static class DictionaryKeyCollectionSerializer +{ + public static IBsonSerializer Create(IBsonSerializer keySerializer, IBsonSerializer valueSerializer) + { + var keyType = keySerializer.ValueType; + var valueType = valueSerializer.ValueType; + var serializerType = typeof(DictionaryKeyCollectionSerializer<,>).MakeGenericType(keyType, valueType); + return (IBsonSerializer)Activator.CreateInstance(serializerType, [keySerializer]); + } +} + +internal class DictionaryKeyCollectionSerializer : EnumerableSerializerBase.KeyCollection> +{ + public DictionaryKeyCollectionSerializer(IBsonSerializer keySerializer) + : base(itemSerializer: keySerializer) + { + } + + protected override void AddItem(object accumulator, object item) => ((Dictionary)accumulator).Add((TKey)item, default(TValue)); + + protected override object CreateAccumulator() => new Dictionary(); + + protected override IEnumerable EnumerateItemsInSerializationOrder(Dictionary.KeyCollection value) => value; + + protected override Dictionary.KeyCollection FinalizeResult(object accumulator) => ((Dictionary)accumulator).Keys; +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionarySerializer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionarySerializer.cs new file mode 100644 index 00000000000..bfecb1ef9c7 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionarySerializer.cs @@ -0,0 +1,52 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Options; +using MongoDB.Bson.Serialization.Serializers; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Serializers; + +internal static class DictionarySerializer +{ + public static IBsonSerializer Create( + DictionaryRepresentation dictionaryRepresentation, + IBsonSerializer keySerializer, + IBsonSerializer valueSerializer) + { + var keyType = keySerializer.ValueType; + var valueType = valueSerializer.ValueType; + var serializerType = typeof(DictionarySerializer<,>).MakeGenericType(keyType, valueType); + return (IBsonSerializer)Activator.CreateInstance(serializerType, [dictionaryRepresentation, keySerializer, valueSerializer]); + } +} + +internal class DictionarySerializer : DictionarySerializerBase, TKey, TValue> +{ + public DictionarySerializer( + DictionaryRepresentation dictionaryRepresentation, + IBsonSerializer keySerializer, + IBsonSerializer valueSerializer) + : base(dictionaryRepresentation, keySerializer, valueSerializer) + { + } + + protected override ICollection> CreateAccumulator() + { + return new Dictionary(); + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryValueCollectionSerializer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryValueCollectionSerializer.cs new file mode 100644 index 00000000000..c3afabebb88 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryValueCollectionSerializer.cs @@ -0,0 +1,57 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Options; +using MongoDB.Bson.Serialization.Serializers; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Serializers; + +internal static class DictionaryValueCollectionSerializer +{ + public static IBsonSerializer Create(IBsonSerializer keySerializer, IBsonSerializer valueSerializer) + { + var keyType = keySerializer.ValueType; + var valueType = valueSerializer.ValueType; + var serializerType = typeof(DictionaryValueCollectionSerializer<,>).MakeGenericType(keyType, valueType); + return (IBsonSerializer)Activator.CreateInstance(serializerType, [keySerializer, valueSerializer]); + } +} + +internal class DictionaryValueCollectionSerializer : SerializerBase.ValueCollection>, IBsonArraySerializer +{ + private readonly IBsonSerializer> _dictionarySerializer; + private readonly IBsonSerializer _wrappedValueSerializer; + + public DictionaryValueCollectionSerializer(IBsonSerializer keySerializer, IBsonSerializer valueSerializer) + { + _dictionarySerializer = (IBsonSerializer>)DictionarySerializer.Create(DictionaryRepresentation.ArrayOfDocuments, keySerializer, valueSerializer); + _wrappedValueSerializer = (IBsonSerializer)KeyValuePairWrappedValueSerializer.Create(keySerializer, valueSerializer); + } + + public override Dictionary.ValueCollection Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var dictionary = _dictionarySerializer.Deserialize(context, args); + return dictionary.Values; + } + + public bool TryGetItemSerializationInfo(out BsonSerializationInfo serializationInfo) + { + serializationInfo = new BsonSerializationInfo(null, _wrappedValueSerializer, typeof(TValue)); + return true; + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/ICollectionSerializer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/ICollectionSerializer.cs new file mode 100644 index 00000000000..ea61e9037d0 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/ICollectionSerializer.cs @@ -0,0 +1,48 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections; +using System.Collections.Generic; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Serializers; + +internal static class ICollectionSerializer +{ + public static IBsonSerializer Create(IBsonSerializer itemSerializer) + { + var itemType = itemSerializer.ValueType; + var serializerType = typeof(ICollectionSerializer<>).MakeGenericType(itemType); + return (IBsonSerializer)Activator.CreateInstance(serializerType, [itemSerializer]); + } +} + +internal class ICollectionSerializer : EnumerableSerializerBase> +{ + public ICollectionSerializer(IBsonSerializer itemSerializer) + : base(itemSerializer) + { + } + + protected override void AddItem(object accumulator, object item) => ((List)accumulator).Add((TItem)item); + + protected override object CreateAccumulator() => new List(); + + protected override IEnumerable EnumerateItemsInSerializationOrder(ICollection value) => value; + + protected override ICollection FinalizeResult(object accumulator) => (ICollection)accumulator; +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/KeyValuePairWrappedValueSerializer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/KeyValuePairWrappedValueSerializer.cs new file mode 100644 index 00000000000..2be915cb095 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/KeyValuePairWrappedValueSerializer.cs @@ -0,0 +1,54 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Serializers; + +internal static class KeyValuePairWrappedValueSerializer +{ + public static IBsonSerializer Create(IBsonSerializer keySerializer, IBsonSerializer valueSerializer) + { + var keyType = keySerializer.ValueType; + var valueType = valueSerializer.ValueType; + var serializerType = typeof(KeyValuePairWrappedValueSerializer<,>).MakeGenericType(keyType, valueType); + return (IBsonSerializer)Activator.CreateInstance(serializerType, [keySerializer, valueSerializer]); + } +} + +internal class KeyValuePairWrappedValueSerializer : SerializerBase, IWrappedValueSerializer +{ + private readonly IBsonSerializer> _keyValuePairSerializer; + private readonly IBsonSerializer _valueSerializer; + + public KeyValuePairWrappedValueSerializer(IBsonSerializer keySerializer, IBsonSerializer valueSerializer) + { + _keyValuePairSerializer = (IBsonSerializer>)KeyValuePairSerializer.Create(BsonType.Document, keySerializer, valueSerializer); + _valueSerializer = valueSerializer; + } + + public string FieldName => "v"; + public IBsonSerializer ValueSerializer => _valueSerializer; + + public override TValue Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var keyValuePair = _keyValuePairSerializer.Deserialize(context, args); + return keyValuePair.Value; + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ExpressionToAggregationExpressionTranslator.cs index 4bfac344596..9f019682a63 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ExpressionToAggregationExpressionTranslator.cs @@ -30,6 +30,12 @@ internal static class ExpressionToAggregationExpressionTranslator { // public static methods public static TranslatedExpression Translate(TranslationContext context, Expression expression) + { + var translatedExpression = TranslateWithoutUnwrapping(context, expression); + return UnwrapIfWrapped(expression, translatedExpression); + } + + public static TranslatedExpression TranslateWithoutUnwrapping(TranslationContext context, Expression expression) { switch (expression.NodeType) { @@ -113,12 +119,11 @@ public static TranslatedExpression TranslateEnumerable(TranslationContext contex { var keySerializer = dictionarySerializer.KeySerializer; var valueSerializer = dictionarySerializer.ValueSerializer; - var keyValuePairSerializer = KeyValuePairSerializer.Create(BsonType.Document, keySerializer, valueSerializer); var ast = AstExpression.ObjectToArray(aggregateExpression.Ast); - var ienumerableSerializer = ArraySerializerHelper.CreateSerializer(keyValuePairSerializer); + var arrayOfDocumentsDictionarySerializer = DictionarySerializer.Create(DictionaryRepresentation.ArrayOfDocuments, keySerializer, valueSerializer); - aggregateExpression = new TranslatedExpression(expression, ast, ienumerableSerializer); + aggregateExpression = new TranslatedExpression(expression, ast, arrayOfDocumentsDictionarySerializer); } return aggregateExpression; @@ -169,5 +174,16 @@ public static TranslatedExpression TranslateLambdaBody( return translatedBody; } + + private static TranslatedExpression UnwrapIfWrapped(Expression expression, TranslatedExpression translatedExpression) + { + if (translatedExpression.Serializer is IWrappedValueSerializer wrappedValueSerializer) + { + var unwrappedAst = AstExpression.GetField(translatedExpression.Ast, wrappedValueSerializer.FieldName); + return new TranslatedExpression(expression, unwrappedAst, wrappedValueSerializer.ValueSerializer); + } + + return translatedExpression; + } } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs index 58cf7f6ab61..b01e5cb3dd3 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs @@ -47,6 +47,11 @@ public static TranslatedExpression Translate(TranslationContext context, MemberE } } + if (TryTranslateDictionaryProperty(context, expression, containerExpression, member, out var translatedDictionaryProperty)) + { + return translatedDictionaryProperty; + } + if (typeof(BsonValue).IsAssignableFrom(containerExpression.Type)) { throw new ExpressionNotSupportedException(expression); // TODO: support BsonValue properties @@ -187,5 +192,75 @@ private static bool TryTranslateDateTimeProperty(MemberExpression expression, Tr return false; } + + private static bool TryTranslateDictionaryProperty(TranslationContext context, MemberExpression expression, Expression containerExpression, MemberInfo memberInfo, out TranslatedExpression translatedDictionaryProperty) + { + if (memberInfo is PropertyInfo propertyInfo) + { + var declaringType = propertyInfo.DeclaringType; + var declaringTypeDefinition = declaringType.IsConstructedGenericType ? declaringType.GetGenericTypeDefinition() : null; + if (declaringTypeDefinition == typeof(Dictionary<,>) || declaringTypeDefinition == typeof(IDictionary<,>)) + { + var containerTranslation = ExpressionToAggregationExpressionTranslator.TranslateEnumerable(context, containerExpression); + var containerAst = containerTranslation.Ast; + + if (containerTranslation.Serializer is IBsonDictionarySerializer dictionarySerializer) + { + var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation; + var keySerializer = dictionarySerializer.KeySerializer; + var valueSerializer = dictionarySerializer.ValueSerializer; + var kvpVar = AstExpression.Var("kvp"); + + switch (propertyInfo.Name) + { + case "Keys": + var keysAst = dictionaryRepresentation switch + { + DictionaryRepresentation.Document or DictionaryRepresentation.ArrayOfDocuments => AstExpression.Map(containerAst, kvpVar, AstExpression.GetField(kvpVar, "k")), + DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstExpression.ArrayElemAt(kvpVar, 0)), + _ => throw new ExpressionNotSupportedException(expression, $"Unexpected dictionary representation: {dictionaryRepresentation}") + }; + var keysSerializer = declaringTypeDefinition == typeof(Dictionary<,>) + ? DictionaryKeyCollectionSerializer.Create(keySerializer, valueSerializer) + : ICollectionSerializer.Create(keySerializer); + translatedDictionaryProperty = new TranslatedExpression(expression, keysAst, keysSerializer); + return true; + + case "Values": + if (declaringTypeDefinition == typeof(Dictionary<,>)) + { + var kvpPairsAst = dictionaryRepresentation switch + { + DictionaryRepresentation.Document or DictionaryRepresentation.ArrayOfDocuments => containerAst, + DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstExpression.ComputedDocument([("k", AstExpression.ArrayElemAt(kvpVar, 0)), ("v", AstExpression.ArrayElemAt(kvpVar, 1))])), + _ => throw new ExpressionNotSupportedException(expression, $"Unexpected dictionary representation: {dictionaryRepresentation}") + }; + var valuesSerializer = DictionaryValueCollectionSerializer.Create(keySerializer, valueSerializer); + translatedDictionaryProperty = new TranslatedExpression(expression, kvpPairsAst, valuesSerializer); + return true; + } + else if (declaringTypeDefinition == typeof(IDictionary<,>)) + { + var valuesAst = dictionaryRepresentation switch + { + DictionaryRepresentation.Document => AstExpression.Map(AstExpression.ObjectToArray(containerAst), kvpVar, AstExpression.GetField(kvpVar, "v")), + DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstComputedArrayExpression.ArrayElemAt(kvpVar, 1)), + DictionaryRepresentation.ArrayOfDocuments => AstExpression.Map(containerAst, kvpVar, AstExpression.GetField(kvpVar, "v")), + _ => throw new ExpressionNotSupportedException(expression, $"Unexpected dictionary representation: {dictionaryRepresentation}") + }; + var valuesSerializer = ICollectionSerializer.Create(valueSerializer); + translatedDictionaryProperty = new TranslatedExpression(expression, valuesAst, valuesSerializer); + return true; + } + break; + } + + } + } + } + + translatedDictionaryProperty = null; + return false; + } } } diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs new file mode 100644 index 00000000000..3092978ce9c --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs @@ -0,0 +1,420 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Linq; +using MongoDB.Driver.TestHelpers; +using FluentAssertions; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson.Serialization.Options; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira; + +public class CSharp5779Tests : LinqIntegrationTest +{ + public CSharp5779Tests(ClassFixture fixture) + : base(fixture) + { + } + + [Fact] + public void DictionaryAsArrayOfArrays_Keys_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsArrayOfArrays.Keys); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $map : { input : '$DictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal("a", "b", "c"); + } + + [Fact] + public void DictionaryAsArrayOfArrays_Keys_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsArrayOfArrays.Keys.First(k => k == "b")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$DictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } }, as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be("b"); + } + + [Fact] + public void DictionaryAsArrayOfArrays_Values_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsArrayOfArrays.Values); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $map : { input : '$DictionaryAsArrayOfArrays', as : 'kvp' in : { k : { $arrayElemAt : ['$$kvp', 0] }, v : { $arrayElemAt : ['$$kvp', 1] } } } } , _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal(1, 2, 3); + } + + [Fact] + public void DictionaryAsArrayOfArrays_Values_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsArrayOfArrays.Values.First(v => v == 2)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$DictionaryAsArrayOfArrays', as : 'kvp', in : { k : { $arrayElemAt : ['$$kvp', 0] }, v : { $arrayElemAt : ['$$kvp', 1] } } } }, as : 'v', cond : { $eq : ['$$v.v', 2] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be(2); + } + + [Fact] + public void DictionaryAsArrayOfDocuments_Keys_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsArrayOfDocuments.Keys); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : '$DictionaryAsArrayOfDocuments.k', _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal("a", "b", "c"); + } + + [Fact] + public void DictionaryAsArrayOfDocuments_Keys_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsArrayOfDocuments.Keys.First(k => k == "b")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : '$DictionaryAsArrayOfDocuments.k', as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be("b"); + } + + [Fact] + public void DictionaryAsArrayOfDocuments_Values_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsArrayOfDocuments.Values); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : '$DictionaryAsArrayOfDocuments', _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal(1, 2, 3); + } + + [Fact] + public void DictionaryAsArrayOfDocuments_Values_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsArrayOfDocuments.Values.First(v => v == 2)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$DictionaryAsArrayOfDocuments', as : 'v', cond : { $eq : ['$$v.v', 2] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be(2); + } + + [Fact] + public void DictionaryAsDocument_Keys_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsDocument.Keys); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $map : { input : { $objectToArray : '$DictionaryAsDocument' }, as : 'kvp', in : '$$kvp.k' } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal("a", "b", "c"); + } + + [Fact] + public void DictionaryAsDocument_Keys_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsDocument.Keys.First(k => k == "b")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : { $objectToArray : '$DictionaryAsDocument' }, as : 'kvp', in : '$$kvp.k' } }, as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be("b"); + } + + [Fact] + public void DictionaryAsDocument_Values_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsDocument.Values); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $objectToArray : '$DictionaryAsDocument' }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal(1, 2, 3); + } + + [Fact] + public void DictionaryAsDocument_Values_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsDocument.Values.First(v => v == 2)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : { $objectToArray : '$DictionaryAsDocument' }, as : 'v', cond : { $eq : ['$$v.v', 2] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be(2); + } + + [Fact] + public void IDictionaryAsArrayOfArrays_Keys_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsArrayOfArrays.Keys); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal("a", "b", "c"); + } + + [Fact] + public void IDictionaryAsArrayOfArrays_Keys_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsArrayOfArrays.Keys.First(k => k == "b")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } }, as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be("b"); + } + + [Fact] + public void IDictionaryAsArrayOfArrays_Values_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsArrayOfArrays.Values); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal(1, 2, 3); + } + + [Fact] + public void IDictionaryAsArrayOfArrays_Values_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsArrayOfArrays.Values.First(v => v == 2)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } , as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be(2); + } + + [Fact] + public void IDictionaryAsArrayOfDocuments_Keys_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsArrayOfDocuments.Keys); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : '$IDictionaryAsArrayOfDocuments.k', _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal("a", "b", "c"); + } + + [Fact] + public void IDictionaryAsArrayOfDocuments_Keys_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsArrayOfDocuments.Keys.First(k => k == "b")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : '$IDictionaryAsArrayOfDocuments.k', as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be("b"); + } + + [Fact] + public void IDictionaryAsArrayOfDocuments_Values_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsArrayOfDocuments.Values); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : '$IDictionaryAsArrayOfDocuments.v', _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal(1, 2, 3); + } + + [Fact] + public void IDictionaryAsArrayOfDocuments_Values_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsArrayOfDocuments.Values.First(v => v == 2)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : '$IDictionaryAsArrayOfDocuments.v', as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be(2); + } + + [Fact] + public void IDictionaryAsDocument_Keys_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsDocument.Keys); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $map : { input : { $objectToArray : '$IDictionaryAsDocument' }, as : 'kvp', in : '$$kvp.k' } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal("a", "b", "c"); + } + + [Fact] + public void IDictionaryAsDocument_Keys_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsDocument.Keys.First(k => k == "b")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : { $objectToArray : '$IDictionaryAsDocument' }, as : 'kvp', in : '$$kvp.k' } }, as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be("b"); + } + + [Fact] + public void IDictionaryAsDocument_Values_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsDocument.Values); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $map : { input : { $objectToArray : '$IDictionaryAsDocument' }, as : 'kvp', in : '$$kvp.v' } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Equal(1, 2, 3); + } + + [Fact] + public void IDictionaryAsDocument_Values_First_with_predicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.IDictionaryAsDocument.Values.First(v => v == 2)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : { $objectToArray : '$IDictionaryAsDocument' }, as : 'kvp', in : '$$kvp.v' } }, as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }"); + + var result = queryable.Single(); + result.Should().Be(2); + } + + public class C + { + public int Id { get; set; } + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] public Dictionary DictionaryAsArrayOfArrays { get; set; } + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] public Dictionary DictionaryAsArrayOfDocuments { get; set; } + [BsonDictionaryOptions(DictionaryRepresentation.Document)] public Dictionary DictionaryAsDocument { get; set; } + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] public IDictionary IDictionaryAsArrayOfArrays { get; set; } + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] public IDictionary IDictionaryAsArrayOfDocuments { get; set; } + [BsonDictionaryOptions(DictionaryRepresentation.Document)] public IDictionary IDictionaryAsDocument { get; set; } + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData => + [ + new C + { + Id = 1, + DictionaryAsArrayOfArrays = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, + DictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, + DictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, + IDictionaryAsArrayOfArrays = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, + IDictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, + IDictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } } + } + ]; + } +} From 6f7337c0366e36650c957f0d1cc1dd7837e94741 Mon Sep 17 00:00:00 2001 From: rstam Date: Sat, 8 Nov 2025 11:00:14 -0500 Subject: [PATCH 2/5] CSHARP-5779: Removed a small amount of unused code --- .../MemberExpressionToAggregationExpressionTranslator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs index b01e5cb3dd3..16ac9162adf 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs @@ -216,7 +216,7 @@ private static bool TryTranslateDictionaryProperty(TranslationContext context, M case "Keys": var keysAst = dictionaryRepresentation switch { - DictionaryRepresentation.Document or DictionaryRepresentation.ArrayOfDocuments => AstExpression.Map(containerAst, kvpVar, AstExpression.GetField(kvpVar, "k")), + DictionaryRepresentation.ArrayOfDocuments => AstExpression.Map(containerAst, kvpVar, AstExpression.GetField(kvpVar, "k")), DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstExpression.ArrayElemAt(kvpVar, 0)), _ => throw new ExpressionNotSupportedException(expression, $"Unexpected dictionary representation: {dictionaryRepresentation}") }; @@ -231,7 +231,7 @@ private static bool TryTranslateDictionaryProperty(TranslationContext context, M { var kvpPairsAst = dictionaryRepresentation switch { - DictionaryRepresentation.Document or DictionaryRepresentation.ArrayOfDocuments => containerAst, + DictionaryRepresentation.ArrayOfDocuments => containerAst, DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstExpression.ComputedDocument([("k", AstExpression.ArrayElemAt(kvpVar, 0)), ("v", AstExpression.ArrayElemAt(kvpVar, 1))])), _ => throw new ExpressionNotSupportedException(expression, $"Unexpected dictionary representation: {dictionaryRepresentation}") }; @@ -243,7 +243,6 @@ private static bool TryTranslateDictionaryProperty(TranslationContext context, M { var valuesAst = dictionaryRepresentation switch { - DictionaryRepresentation.Document => AstExpression.Map(AstExpression.ObjectToArray(containerAst), kvpVar, AstExpression.GetField(kvpVar, "v")), DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstComputedArrayExpression.ArrayElemAt(kvpVar, 1)), DictionaryRepresentation.ArrayOfDocuments => AstExpression.Map(containerAst, kvpVar, AstExpression.GetField(kvpVar, "v")), _ => throw new ExpressionNotSupportedException(expression, $"Unexpected dictionary representation: {dictionaryRepresentation}") From 711144310c63a2bd5cd0586e38421b04e2770a1c Mon Sep 17 00:00:00 2001 From: rstam Date: Mon, 10 Nov 2025 17:23:06 -0500 Subject: [PATCH 3/5] CSHARP-5779: Requested changes --- .../Serializers/DictionaryKeyCollectionSerializer.cs | 2 +- .../Serializers/DictionaryValueCollectionSerializer.cs | 2 +- .../Linq3Implementation/Serializers/ICollectionSerializer.cs | 2 +- .../Serializers/KeyValuePairWrappedValueSerializer.cs | 2 +- .../MemberExpressionToAggregationExpressionTranslator.cs | 2 +- .../Linq/Linq3Implementation/Jira/CSharp5779Tests.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryKeyCollectionSerializer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryKeyCollectionSerializer.cs index 836262a6bd8..39b0d72a2d8 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryKeyCollectionSerializer.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryKeyCollectionSerializer.cs @@ -27,7 +27,7 @@ public static IBsonSerializer Create(IBsonSerializer keySerializer, IBsonSeriali { var keyType = keySerializer.ValueType; var valueType = valueSerializer.ValueType; - var serializerType = typeof(DictionaryKeyCollectionSerializer<,>).MakeGenericType(keyType, valueType); + var serializerType = typeof(DictionaryKeyCollectionSerializer<,>).MakeGenericType(keyType, valueType); return (IBsonSerializer)Activator.CreateInstance(serializerType, [keySerializer]); } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryValueCollectionSerializer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryValueCollectionSerializer.cs index c3afabebb88..e1ee050d9cd 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryValueCollectionSerializer.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/DictionaryValueCollectionSerializer.cs @@ -27,7 +27,7 @@ public static IBsonSerializer Create(IBsonSerializer keySerializer, IBsonSeriali { var keyType = keySerializer.ValueType; var valueType = valueSerializer.ValueType; - var serializerType = typeof(DictionaryValueCollectionSerializer<,>).MakeGenericType(keyType, valueType); + var serializerType = typeof(DictionaryValueCollectionSerializer<,>).MakeGenericType(keyType, valueType); return (IBsonSerializer)Activator.CreateInstance(serializerType, [keySerializer, valueSerializer]); } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/ICollectionSerializer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/ICollectionSerializer.cs index ea61e9037d0..065fd446535 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/ICollectionSerializer.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/ICollectionSerializer.cs @@ -26,7 +26,7 @@ internal static class ICollectionSerializer public static IBsonSerializer Create(IBsonSerializer itemSerializer) { var itemType = itemSerializer.ValueType; - var serializerType = typeof(ICollectionSerializer<>).MakeGenericType(itemType); + var serializerType = typeof(ICollectionSerializer<>).MakeGenericType(itemType); return (IBsonSerializer)Activator.CreateInstance(serializerType, [itemSerializer]); } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/KeyValuePairWrappedValueSerializer.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/KeyValuePairWrappedValueSerializer.cs index 2be915cb095..b2fee08e360 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/KeyValuePairWrappedValueSerializer.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Serializers/KeyValuePairWrappedValueSerializer.cs @@ -27,7 +27,7 @@ public static IBsonSerializer Create(IBsonSerializer keySerializer, IBsonSeriali { var keyType = keySerializer.ValueType; var valueType = valueSerializer.ValueType; - var serializerType = typeof(KeyValuePairWrappedValueSerializer<,>).MakeGenericType(keyType, valueType); + var serializerType = typeof(KeyValuePairWrappedValueSerializer<,>).MakeGenericType(keyType, valueType); return (IBsonSerializer)Activator.CreateInstance(serializerType, [keySerializer, valueSerializer]); } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs index 16ac9162adf..0a0a9b42897 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs @@ -243,7 +243,7 @@ private static bool TryTranslateDictionaryProperty(TranslationContext context, M { var valuesAst = dictionaryRepresentation switch { - DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstComputedArrayExpression.ArrayElemAt(kvpVar, 1)), + DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstExpression.ArrayElemAt(kvpVar, 1)), DictionaryRepresentation.ArrayOfDocuments => AstExpression.Map(containerAst, kvpVar, AstExpression.GetField(kvpVar, "v")), _ => throw new ExpressionNotSupportedException(expression, $"Unexpected dictionary representation: {dictionaryRepresentation}") }; diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs index 3092978ce9c..0d3a5c84cf6 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs @@ -264,7 +264,7 @@ public void IDictionaryAsArrayOfArrays_Values_First_with_predicate_should_work() .Select(x => x.IDictionaryAsArrayOfArrays.Values.First(v => v == 2)); var stages = Translate(collection, queryable); - AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } , as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }"); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } }, as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }"); var result = queryable.Single(); result.Should().Be(2); From 40372f3059a9aa41fccf79928f11e591d284d80e Mon Sep 17 00:00:00 2001 From: rstam Date: Mon, 10 Nov 2025 18:00:23 -0500 Subject: [PATCH 4/5] CSHARP-5779: Expand tests to cover dictionaries with 0-3 values --- .../Jira/CSharp5779Tests.cs | 224 ++++++++++++++---- 1 file changed, 175 insertions(+), 49 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs index 0d3a5c84cf6..2ad443cc51b 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs @@ -41,8 +41,12 @@ public void DictionaryAsArrayOfArrays_Keys_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $map : { input : '$DictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal("a", "b", "c"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal("a"); + results[2].Should().Equal("a", "b"); + results[3].Should().Equal("a", "b", "c"); } [Fact] @@ -56,8 +60,12 @@ public void DictionaryAsArrayOfArrays_Keys_First_with_predicate_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$DictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } }, as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be("b"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(null); + results[1].Should().Be(null); + results[2].Should().Be("b"); + results[3].Should().Be("b"); } [Fact] @@ -71,8 +79,12 @@ public void DictionaryAsArrayOfArrays_Values_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $map : { input : '$DictionaryAsArrayOfArrays', as : 'kvp' in : { k : { $arrayElemAt : ['$$kvp', 0] }, v : { $arrayElemAt : ['$$kvp', 1] } } } } , _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal(1, 2, 3); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal(1); + results[2].Should().Equal(1, 2); + results[3].Should().Equal(1, 2, 3); } [Fact] @@ -86,8 +98,12 @@ public void DictionaryAsArrayOfArrays_Values_First_with_predicate_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$DictionaryAsArrayOfArrays', as : 'kvp', in : { k : { $arrayElemAt : ['$$kvp', 0] }, v : { $arrayElemAt : ['$$kvp', 1] } } } }, as : 'v', cond : { $eq : ['$$v.v', 2] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be(2); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(0); + results[1].Should().Be(0); + results[2].Should().Be(2); + results[3].Should().Be(2); } [Fact] @@ -101,8 +117,12 @@ public void DictionaryAsArrayOfDocuments_Keys_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : '$DictionaryAsArrayOfDocuments.k', _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal("a", "b", "c"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal("a"); + results[2].Should().Equal("a", "b"); + results[3].Should().Equal("a", "b", "c"); } [Fact] @@ -116,8 +136,12 @@ public void DictionaryAsArrayOfDocuments_Keys_First_with_predicate_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : '$DictionaryAsArrayOfDocuments.k', as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be("b"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(null); + results[1].Should().Be(null); + results[2].Should().Be("b"); + results[3].Should().Be("b"); } [Fact] @@ -131,8 +155,12 @@ public void DictionaryAsArrayOfDocuments_Values_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : '$DictionaryAsArrayOfDocuments', _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal(1, 2, 3); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal(1); + results[2].Should().Equal(1, 2); + results[3].Should().Equal(1, 2, 3); } [Fact] @@ -146,8 +174,12 @@ public void DictionaryAsArrayOfDocuments_Values_First_with_predicate_should_work var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$DictionaryAsArrayOfDocuments', as : 'v', cond : { $eq : ['$$v.v', 2] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be(2); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(0); + results[1].Should().Be(0); + results[2].Should().Be(2); + results[3].Should().Be(2); } [Fact] @@ -161,8 +193,12 @@ public void DictionaryAsDocument_Keys_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $map : { input : { $objectToArray : '$DictionaryAsDocument' }, as : 'kvp', in : '$$kvp.k' } }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal("a", "b", "c"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal("a"); + results[2].Should().Equal("a", "b"); + results[3].Should().Equal("a", "b", "c"); } [Fact] @@ -176,8 +212,12 @@ public void DictionaryAsDocument_Keys_First_with_predicate_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : { $objectToArray : '$DictionaryAsDocument' }, as : 'kvp', in : '$$kvp.k' } }, as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be("b"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(null); + results[1].Should().Be(null); + results[2].Should().Be("b"); + results[3].Should().Be("b"); } [Fact] @@ -191,8 +231,12 @@ public void DictionaryAsDocument_Values_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $objectToArray : '$DictionaryAsDocument' }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal(1, 2, 3); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal(1); + results[2].Should().Equal(1, 2); + results[3].Should().Equal(1, 2, 3); } [Fact] @@ -206,8 +250,12 @@ public void DictionaryAsDocument_Values_First_with_predicate_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : { $objectToArray : '$DictionaryAsDocument' }, as : 'v', cond : { $eq : ['$$v.v', 2] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be(2); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(0); + results[1].Should().Be(0); + results[2].Should().Be(2); + results[3].Should().Be(2); } [Fact] @@ -221,8 +269,12 @@ public void IDictionaryAsArrayOfArrays_Keys_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal("a", "b", "c"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal("a"); + results[2].Should().Equal("a", "b"); + results[3].Should().Equal("a", "b", "c"); } [Fact] @@ -236,8 +288,12 @@ public void IDictionaryAsArrayOfArrays_Keys_First_with_predicate_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } }, as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be("b"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(null); + results[1].Should().Be(null); + results[2].Should().Be("b"); + results[3].Should().Be("b"); } [Fact] @@ -251,8 +307,12 @@ public void IDictionaryAsArrayOfArrays_Values_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal(1, 2, 3); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal(1); + results[2].Should().Equal(1, 2); + results[3].Should().Equal(1, 2, 3); } [Fact] @@ -266,8 +326,12 @@ public void IDictionaryAsArrayOfArrays_Values_First_with_predicate_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } }, as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be(2); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(0); + results[1].Should().Be(0); + results[2].Should().Be(2); + results[3].Should().Be(2); } [Fact] @@ -281,8 +345,12 @@ public void IDictionaryAsArrayOfDocuments_Keys_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : '$IDictionaryAsArrayOfDocuments.k', _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal("a", "b", "c"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal("a"); + results[2].Should().Equal("a", "b"); + results[3].Should().Equal("a", "b", "c"); } [Fact] @@ -296,8 +364,12 @@ public void IDictionaryAsArrayOfDocuments_Keys_First_with_predicate_should_work( var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : '$IDictionaryAsArrayOfDocuments.k', as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be("b"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(null); + results[1].Should().Be(null); + results[2].Should().Be("b"); + results[3].Should().Be("b"); } [Fact] @@ -311,8 +383,12 @@ public void IDictionaryAsArrayOfDocuments_Values_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : '$IDictionaryAsArrayOfDocuments.v', _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal(1, 2, 3); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal(1); + results[2].Should().Equal(1, 2); + results[3].Should().Equal(1, 2, 3); } [Fact] @@ -326,8 +402,12 @@ public void IDictionaryAsArrayOfDocuments_Values_First_with_predicate_should_wor var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : '$IDictionaryAsArrayOfDocuments.v', as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be(2); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(0); + results[1].Should().Be(0); + results[2].Should().Be(2); + results[3].Should().Be(2); } [Fact] @@ -341,8 +421,12 @@ public void IDictionaryAsDocument_Keys_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $map : { input : { $objectToArray : '$IDictionaryAsDocument' }, as : 'kvp', in : '$$kvp.k' } }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal("a", "b", "c"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal("a"); + results[2].Should().Equal("a", "b"); + results[3].Should().Equal("a", "b", "c"); } [Fact] @@ -356,8 +440,12 @@ public void IDictionaryAsDocument_Keys_First_with_predicate_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : { $objectToArray : '$IDictionaryAsDocument' }, as : 'kvp', in : '$$kvp.k' } }, as : 'k', cond : { $eq : ['$$k', 'b'] } } }, 0] }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be("b"); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(null); + results[1].Should().Be(null); + results[2].Should().Be("b"); + results[3].Should().Be("b"); } [Fact] @@ -371,8 +459,12 @@ public void IDictionaryAsDocument_Values_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $map : { input : { $objectToArray : '$IDictionaryAsDocument' }, as : 'kvp', in : '$$kvp.v' } }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Equal(1, 2, 3); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal(1); + results[2].Should().Equal(1, 2); + results[3].Should().Equal(1, 2, 3); } [Fact] @@ -386,8 +478,12 @@ public void IDictionaryAsDocument_Values_First_with_predicate_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : { $objectToArray : '$IDictionaryAsDocument' }, as : 'kvp', in : '$$kvp.v' } }, as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }"); - var result = queryable.Single(); - result.Should().Be(2); + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Be(0); + results[1].Should().Be(0); + results[2].Should().Be(2); + results[3].Should().Be(2); } public class C @@ -408,13 +504,43 @@ public sealed class ClassFixture : MongoCollectionFixture new C { Id = 1, + DictionaryAsArrayOfArrays = new Dictionary(), + DictionaryAsArrayOfDocuments = new Dictionary(), + DictionaryAsDocument = new Dictionary(), + IDictionaryAsArrayOfArrays = new Dictionary(), + IDictionaryAsArrayOfDocuments = new Dictionary(), + IDictionaryAsDocument = new Dictionary() + }, + new C + { + Id = 2, + DictionaryAsArrayOfArrays = new Dictionary { { "a", 1 } }, + DictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 } }, + DictionaryAsDocument = new Dictionary { { "a", 1 } }, + IDictionaryAsArrayOfArrays = new Dictionary { { "a", 1 } }, + IDictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 } }, + IDictionaryAsDocument = new Dictionary { { "a", 1 } } + }, + new C + { + Id = 3, + DictionaryAsArrayOfArrays = new Dictionary { { "a", 1 }, { "b", 2 } }, + DictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 }, { "b", 2 } }, + DictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 } }, + IDictionaryAsArrayOfArrays = new Dictionary { { "a", 1 }, { "b", 2 } }, + IDictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 }, { "b", 2 } }, + IDictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 }, } + }, + new C + { + Id = 4, DictionaryAsArrayOfArrays = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, DictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, DictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, IDictionaryAsArrayOfArrays = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, IDictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, IDictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } } - } + }, ]; } } From 15113cad9f11a6e8019c9f8e758847a1b5d2d48e Mon Sep 17 00:00:00 2001 From: rstam Date: Tue, 11 Nov 2025 11:22:54 -0500 Subject: [PATCH 5/5] CSHARP-5779: Added tests for dictionaries with nested dictionary values --- ...MethodToAggregationExpressionTranslator.cs | 5 +- .../Jira/CSharp5779Tests.cs | 119 +++++++++++++++++- 2 files changed, 119 insertions(+), 5 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SelectManyMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SelectManyMethodToAggregationExpressionTranslator.cs index 2eb826a8770..89b67968c24 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SelectManyMethodToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/SelectManyMethodToAggregationExpressionTranslator.cs @@ -19,6 +19,7 @@ using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; using MongoDB.Driver.Linq.Linq3Implementation.Misc; using MongoDB.Driver.Linq.Linq3Implementation.Reflection; +using MongoDB.Driver.Linq.Linq3Implementation.Serializers; namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators { @@ -47,6 +48,7 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC var selectorParameterSymbol = context.CreateSymbol(selectorParameter, selectorParameterSerializer); var selectorContext = context.WithSymbol(selectorParameterSymbol); var selectorTranslation = ExpressionToAggregationExpressionTranslator.Translate(selectorContext, selectorLambda.Body); + var itemSerializer = ArraySerializerHelper.GetItemSerializer(selectorTranslation.Serializer); var asVar = selectorParameterSymbol.Var; var valueVar = AstExpression.Var("value"); @@ -59,7 +61,8 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC initialValue: new BsonArray(), @in: AstExpression.ConcatArrays(valueVar, thisVar)); - return new TranslatedExpression(expression, ast, selectorTranslation.Serializer); + var ienumerableSerializer = IEnumerableSerializer.Create(itemSerializer); + return new TranslatedExpression(expression, ast, ienumerableSerializer); } throw new ExpressionNotSupportedException(expression); diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs index 2ad443cc51b..cc7a78eb37c 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5779Tests.cs @@ -17,14 +17,29 @@ using System.Linq; using MongoDB.Driver.TestHelpers; using FluentAssertions; +using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Options; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver.Linq.Linq3Implementation.Serializers; using Xunit; namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira; public class CSharp5779Tests : LinqIntegrationTest { + static CSharp5779Tests() + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + + var innerDictionarySerializer = DictionarySerializer.Create(DictionaryRepresentation.ArrayOfArrays, StringSerializer.Instance, Int32Serializer.Instance); + var outerDictionarySerializer = DictionarySerializer.Create(DictionaryRepresentation.Document, StringSerializer.Instance, innerDictionarySerializer); + cm.MapMember(c => c.DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays).SetSerializer(outerDictionarySerializer); + }); + } + public CSharp5779Tests(ClassFixture fixture) : base(fixture) { @@ -486,6 +501,82 @@ public void IDictionaryAsDocument_Values_First_with_predicate_should_work() results[3].Should().Be(2); } + [Fact] + public void DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays_Values_SelectMany_Keys_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays.Values.SelectMany(n => n.Keys)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : { $map : { input : { $objectToArray : '$DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays' }, as : 'n', in : { $map : { input : '$$n.v', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } } } }, initialValue : [], in : { $concatArrays : ['$$value', '$$this'] } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal("a"); + results[2].Should().Equal("a", "a", "b"); + results[3].Should().Equal("a", "a", "b", "a", "b", "c"); + } + + [Fact] + public void DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays_Values_SelectMany_Keys_Where_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays.Values.SelectMany(n => n.Keys.Where(k => k != "a"))); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : { $map : { input : { $objectToArray : '$DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays' }, as : 'n', in : { $filter : { input : { $map : { input : '$$n.v', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } }, as : 'k', cond : { $ne : ['$$k', 'a'] } } } } }, initialValue : [], in : { $concatArrays : ['$$value', '$$this'] } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal(); + results[2].Should().Equal("b"); + results[3].Should().Equal("b", "b", "c"); + } + + [Fact] + public void DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays_Values_SelectMany_Values_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays.Values.SelectMany(n => n.Values)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : { $map : { input : { $objectToArray : '$DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays' }, as : 'n', in : { $map : { input : '$$n.v', as : 'kvp', in : { k : { $arrayElemAt : ['$$kvp', 0] }, v : { $arrayElemAt : ['$$kvp', 1] } } } } } }, initialValue : [], in : { $concatArrays : ['$$value', '$$this'] } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal(1); + results[2].Should().Equal(1, 1, 2); + results[3].Should().Equal(1, 1, 2, 1, 2, 3); + } + + [Fact] + public void DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays_Values_SelectMany_Values_Where_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays.Values.SelectMany(n => n.Values.Where(v => v > 1))); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : { $map : { input : { $objectToArray : '$DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays' }, as : 'n', in : { $filter : { input : { $map : { input : '$$n.v', as : 'kvp', in : { k : { $arrayElemAt : ['$$kvp', 0] }, v : { $arrayElemAt : ['$$kvp', 1] } } } }, as : 'v', cond : { $gt : ['$$v.v', 1] } } } } }, initialValue : [], in : { $concatArrays : ['$$value', '$$this'] } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Count.Should().Be(4); + results[0].Should().Equal(); + results[1].Should().Equal(); + results[2].Should().Equal(2); + results[3].Should().Equal(2, 2, 3); + } + public class C { public int Id { get; set; } @@ -495,6 +586,7 @@ public class C [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] public IDictionary IDictionaryAsArrayOfArrays { get; set; } [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] public IDictionary IDictionaryAsArrayOfDocuments { get; set; } [BsonDictionaryOptions(DictionaryRepresentation.Document)] public IDictionary IDictionaryAsDocument { get; set; } + public Dictionary> DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays { get; set; } } public sealed class ClassFixture : MongoCollectionFixture @@ -509,7 +601,8 @@ public sealed class ClassFixture : MongoCollectionFixture DictionaryAsDocument = new Dictionary(), IDictionaryAsArrayOfArrays = new Dictionary(), IDictionaryAsArrayOfDocuments = new Dictionary(), - IDictionaryAsDocument = new Dictionary() + IDictionaryAsDocument = new Dictionary(), + DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays = new Dictionary>() }, new C { @@ -519,7 +612,12 @@ public sealed class ClassFixture : MongoCollectionFixture DictionaryAsDocument = new Dictionary { { "a", 1 } }, IDictionaryAsArrayOfArrays = new Dictionary { { "a", 1 } }, IDictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 } }, - IDictionaryAsDocument = new Dictionary { { "a", 1 } } + IDictionaryAsDocument = new Dictionary { { "a", 1 } }, + DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays = new Dictionary> + { + { "", new Dictionary() }, + { "a", new Dictionary { { "a", 1 } } } + } }, new C { @@ -529,7 +627,13 @@ public sealed class ClassFixture : MongoCollectionFixture DictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 } }, IDictionaryAsArrayOfArrays = new Dictionary { { "a", 1 }, { "b", 2 } }, IDictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 }, { "b", 2 } }, - IDictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 }, } + IDictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 }, }, + DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays = new Dictionary> + { + { "", new Dictionary() }, + { "a", new Dictionary { { "a", 1 } } }, + { "b", new Dictionary { { "a", 1 }, { "b", 2 } } } + } }, new C { @@ -539,7 +643,14 @@ public sealed class ClassFixture : MongoCollectionFixture DictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, IDictionaryAsArrayOfArrays = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, IDictionaryAsArrayOfDocuments = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, - IDictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } } + IDictionaryAsDocument = new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } }, + DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays = new Dictionary> + { + { "", new Dictionary() }, + { "a", new Dictionary { { "a", 1 } } }, + { "b", new Dictionary { { "a", 1 }, { "b", 2 } } }, + { "c", new Dictionary { { "a", 1 }, { "b", 2 }, { "c", 3 } } } + } }, ]; }