diff --git a/src/MongoDB.Bson/Serialization/Serializers/DictionarySerializerBase.cs b/src/MongoDB.Bson/Serialization/Serializers/DictionarySerializerBase.cs index 96b708d8aa6..3f6ff642bdd 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/DictionarySerializerBase.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/DictionarySerializerBase.cs @@ -499,21 +499,20 @@ obj is DictionarySerializerBase other && /// public bool TryGetItemSerializationInfo(out BsonSerializationInfo serializationInfo) { - if (_dictionaryRepresentation != DictionaryRepresentation.ArrayOfDocuments) + if (_dictionaryRepresentation is DictionaryRepresentation.ArrayOfArrays or DictionaryRepresentation.ArrayOfDocuments) { - serializationInfo = null; - return false; + var representation = _dictionaryRepresentation == DictionaryRepresentation.ArrayOfArrays + ? BsonType.Array + : BsonType.Document; + var keySerializer = _lazyKeySerializer.Value; + var valueSerializer = _lazyValueSerializer.Value; + var keyValuePairSerializer = new KeyValuePairSerializer(representation, keySerializer, valueSerializer); + serializationInfo = new BsonSerializationInfo(null, keyValuePairSerializer, keyValuePairSerializer.ValueType); + return true; } - var serializer = new KeyValuePairSerializer( - BsonType.Document, - _lazyKeySerializer.Value, - _lazyValueSerializer.Value); - serializationInfo = new BsonSerializationInfo( - null, - serializer, - serializer.ValueType); - return true; + serializationInfo = null; + return false; } /// diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ExpressionToAggregationExpressionTranslator.cs index c2d8e0010e9..4bfac344596 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ExpressionToAggregationExpressionTranslator.cs @@ -16,7 +16,9 @@ using System; 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.Ast.Expressions; using MongoDB.Driver.Linq.Linq3Implementation.Misc; @@ -95,15 +97,28 @@ public static TranslatedExpression TranslateEnumerable(TranslationContext contex { var aggregateExpression = Translate(context, expression); - var serializer = aggregateExpression.Serializer; - if (serializer is IWrappedEnumerableSerializer wrappedEnumerableSerializer) + if (aggregateExpression.Serializer is IWrappedEnumerableSerializer wrappedEnumerableSerializer) { var enumerableFieldName = wrappedEnumerableSerializer.EnumerableFieldName; var enumerableElementSerializer = wrappedEnumerableSerializer.EnumerableElementSerializer; - var enumerableSerializer = IEnumerableSerializer.Create(enumerableElementSerializer); + var ast = AstExpression.GetField(aggregateExpression.Ast, enumerableFieldName); + var ienumerableSerializer = IEnumerableSerializer.Create(enumerableElementSerializer); + + aggregateExpression = new TranslatedExpression(expression, ast, ienumerableSerializer); + } + + if (aggregateExpression.Serializer is IBsonDictionarySerializer dictionarySerializer && + dictionarySerializer.DictionaryRepresentation == DictionaryRepresentation.Document) + { + 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); - return new TranslatedExpression(aggregateExpression.Expression, ast, enumerableSerializer); + aggregateExpression = new TranslatedExpression(expression, ast, ienumerableSerializer); } return aggregateExpression; diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/SelectManyMethodToPipelineTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/SelectManyMethodToPipelineTranslator.cs index 309c7a2e5fa..c90f7796140 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/SelectManyMethodToPipelineTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/SelectManyMethodToPipelineTranslator.cs @@ -14,8 +14,13 @@ */ using System.Collections.ObjectModel; +using System.Linq; using System.Linq.Expressions; using System.Reflection; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Options; +using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.Linq.Linq3Implementation.Ast; using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; using MongoDB.Driver.Linq.Linq3Implementation.Ast.Stages; @@ -74,16 +79,23 @@ private static TranslatedPipeline TranslateSelectMany( { var sourceSerializer = pipeline.OutputSerializer; var selectorLambda = ExpressionHelper.UnquoteLambda(arguments[1]); - var selectorTranslation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, selectorLambda, sourceSerializer, asRoot: true); - var resultValueSerializer = ArraySerializerHelper.GetItemSerializer(selectorTranslation.Serializer); - var resultWrappedValueSerializer = WrappedValueSerializer.Create("_v", resultValueSerializer); + var selectorParameter = selectorLambda.Parameters.Single(); + var selectorParameterSymbol = context.CreateSymbol(selectorParameter, sourceSerializer, isCurrent: true); + var selectorContext = context.WithSymbol(selectorParameterSymbol); + var selectorTranslation = ExpressionToAggregationExpressionTranslator.TranslateEnumerable(selectorContext, selectorLambda.Body); + + var valuesAst = selectorTranslation.Ast; + var valuesSerializer = selectorTranslation.Serializer; + + var valuesItemSerializer = ArraySerializerHelper.GetItemSerializer(valuesSerializer); + var wrappedValueSerializer = WrappedValueSerializer.Create("_v", valuesItemSerializer); pipeline = pipeline.AddStages( AstStage.Project( - AstProject.Set("_v", selectorTranslation.Ast), + AstProject.Set("_v", valuesAst), AstProject.ExcludeId()), AstStage.Unwind("_v"), - resultWrappedValueSerializer); + wrappedValueSerializer); return pipeline; } diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp1913Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp1913Tests.cs new file mode 100644 index 00000000000..0e5bc7f1c2d --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp1913Tests.cs @@ -0,0 +1,203 @@ +/* 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 CSharp1913Tests : LinqIntegrationTest +{ + public CSharp1913Tests(ClassFixture fixture) + : base(fixture) + { + } + + [Fact] + public void Nested__SelectMany_with_ArrayOfArrays_representation_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .OfType() + .Where(to => to.Name == "TestName") + .Select(to => to.DictionaryWithArrayOfArraysRepresentation.SelectMany(kvp => new KeyValuePair[] { kvp })); + + var stages = Translate(collection, queryable); + AssertStages( + stages, + "{ $match : { Name : 'TestName' } }", + "{ $project : { _v : { $reduce : { input : { $map : { input : '$DictionaryWithArrayOfArraysRepresentation', as : 'kvp', in : ['$$kvp'] } }, initialValue : [], in : { $concatArrays : ['$$value', '$$this'] } } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Select(kvp => kvp.Key).Should().Equal("A", "B", "C"); + } + + [Fact] + public void Nested__SelectMany_with_ArrayOfDocuments_representation_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .OfType() + .Where(to => to.Name == "TestName") + .Select(to => to.DictionaryWithArrayOfDocumentsRepresentation.SelectMany(kvp => new KeyValuePair[] { kvp })); + + var stages = Translate(collection, queryable); + AssertStages( + stages, + "{ $match : { Name : 'TestName' } }", + "{ $project : { _v : { $reduce : { input : { $map : { input : '$DictionaryWithArrayOfDocumentsRepresentation', as : 'kvp', in : ['$$kvp'] } }, initialValue : [], in : { $concatArrays : ['$$value', '$$this'] } } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Select(kvp => kvp.Key).Should().Equal("A", "B", "C"); + } + + [Fact] + public void Nested_SelectMany_with_Document_representation_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .OfType() + .Where(to => to.Name == "TestName") + .Select(to => to.DictionaryWithDocumentRepresentation.SelectMany(kvp => new KeyValuePair[] { kvp })); + + var stages = Translate(collection, queryable); + AssertStages( + stages, + "{ $match : { Name : 'TestName' } }", + "{ $project : { _v : { $reduce : { input : { $map : { input : { $objectToArray : '$DictionaryWithDocumentRepresentation' }, as : 'kvp', in : ['$$kvp'] } }, initialValue : [], in : { $concatArrays : ['$$value', '$$this'] } } }, _id : 0 } }"); + + var result = queryable.Single(); + result.Select(kvp => kvp.Key).Should().Equal("A", "B", "C"); + } + + [Fact] + public void Top_level_SelectMany_with_ArrayOfArrays_representation_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .OfType() + .Where(to => to.Name == "TestName") + .SelectMany(to => to.DictionaryWithArrayOfArraysRepresentation); + + var stages = Translate(collection, queryable); + AssertStages( + stages, + "{ $match : { Name : 'TestName' } }", + "{ $project : { _v : '$DictionaryWithArrayOfArraysRepresentation', _id : 0 } }", + "{ $unwind : '$_v' }"); + + var results = queryable.ToList(); + results.Count.Should().Be(3); + results[0].Key.Should().Be("A"); + results[0].Value.Should().Be("a"); + results[1].Key.Should().Be("B"); + results[1].Value.Should().Be("b"); + results[2].Key.Should().Be("C"); + results[2].Value.Should().Be("c"); + } + + [Fact] + public void Top_level_SelectMany_with_ArrayOfDocuments_representation_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .OfType() + .Where(to => to.Name == "TestName") + .SelectMany(to => to.DictionaryWithArrayOfDocumentsRepresentation); + + var stages = Translate(collection, queryable); + AssertStages( + stages, + "{ $match : { Name : 'TestName' } }", + "{ $project : { _v : '$DictionaryWithArrayOfDocumentsRepresentation', _id : 0 } }", + "{ $unwind : '$_v' }"); + + var results = queryable.ToList(); + results.Count.Should().Be(3); + results[0].Key.Should().Be("A"); + results[0].Value.Should().Be("a"); + results[1].Key.Should().Be("B"); + results[1].Value.Should().Be("b"); + results[2].Key.Should().Be("C"); + results[2].Value.Should().Be("c"); + } + + [Fact] + public void Top_level_SelectMany_with_Document_representation_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .OfType() + .Where(to => to.Name == "TestName") + .SelectMany(to => to.DictionaryWithDocumentRepresentation); + + var stages = Translate(collection, queryable); + AssertStages( + stages, + "{ $match : { Name : 'TestName' } }", + "{ $project : { _v : { $objectToArray : '$DictionaryWithDocumentRepresentation' }, _id : 0 } }", + "{ $unwind : '$_v' }"); + + var results = queryable.ToList(); + results.Count.Should().Be(3); + results[0].Key.Should().Be("A"); + results[0].Value.Should().Be("a"); + results[1].Key.Should().Be("B"); + results[1].Value.Should().Be("b"); + results[2].Key.Should().Be("C"); + results[2].Value.Should().Be("c"); + } + + public class C + { + public int Id { get; set; } + public string Name {get;set;} + + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] + public Dictionary DictionaryWithArrayOfArraysRepresentation { get; set; } = new Dictionary(); + + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] + public Dictionary DictionaryWithArrayOfDocumentsRepresentation { get; set; } = new Dictionary(); + + [BsonDictionaryOptions(DictionaryRepresentation.Document)] + public Dictionary DictionaryWithDocumentRepresentation { get; set; } = new Dictionary(); + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData => + [ + new C + { + Id = 1, + Name = "TestName", + DictionaryWithArrayOfArraysRepresentation = new Dictionary { { "A", "a" }, { "B", "b" }, { "C", "c" } }, + DictionaryWithArrayOfDocumentsRepresentation = new Dictionary { { "A", "a" }, { "B", "b" }, { "C", "c" } }, + DictionaryWithDocumentRepresentation = new Dictionary { { "A", "a" }, { "B", "b" }, { "C", "c" } }, + } + ]; + } +}