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..16ac9162adf 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,74 @@ 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.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.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.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 } } + } + ]; + } +}