From b66daf11c77cd491dd1d8a607f3c16e4d98fc4ce Mon Sep 17 00:00:00 2001 From: adelinowona Date: Thu, 30 Oct 2025 20:39:03 -0400 Subject: [PATCH 1/2] CSHARP-4443: Add comprehensive dictionary LINQ support for all 3 representations --- .../Serializers/KeyValuePairSerializer.cs | 28 ++- .../Ast/Expressions/AstExpression.cs | 5 + ...essionToAggregationExpressionTranslator.cs | 165 +++++++++++++++++- ...MethodToAggregationExpressionTranslator.cs | 62 +++++-- ...MethodToAggregationExpressionTranslator.cs | 49 ++++++ ...MethodToAggregationExpressionTranslator.cs | 96 +++++----- ...MethodToAggregationExpressionTranslator.cs | 26 ++- .../AllOrAnyMethodToFilterTranslator.cs | 8 +- .../ContainsKeyMethodToFilterTranslator.cs | 38 ++-- .../ContainsMethodToFilterTranslator.cs | 60 ++++++- .../ContainsValueMethodToFilterTranslator.cs | 40 +++-- ...MemberExpressionToFilterFieldTranslator.cs | 31 +++- 12 files changed, 509 insertions(+), 99 deletions(-) diff --git a/src/MongoDB.Bson/Serialization/Serializers/KeyValuePairSerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/KeyValuePairSerializer.cs index 1ff39685b2a..43591d7e92b 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/KeyValuePairSerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/KeyValuePairSerializer.cs @@ -29,6 +29,22 @@ public interface IKeyValuePairSerializer BsonType Representation { get; } } + /// + /// An extended interface for KeyValuePairSerializer that provides access to key and value serializers. + /// + public interface IKeyValuePairSerializerV2 : IKeyValuePairSerializer + { + /// + /// Gets the key serializer. + /// + IBsonSerializer KeySerializer { get; } + + /// + /// Gets the value serializer. + /// + IBsonSerializer ValueSerializer { get; } + } + /// /// Static factory class for KeyValuePairSerializers. /// @@ -61,7 +77,7 @@ public static IBsonSerializer Create( public sealed class KeyValuePairSerializer : StructSerializerBase>, IBsonDocumentSerializer, - IKeyValuePairSerializer + IKeyValuePairSerializerV2 { // private constants private static class Flags @@ -191,6 +207,16 @@ public IBsonSerializer ValueSerializer get { return _lazyValueSerializer.Value; } } + /// + /// Gets the key serializer. + /// + IBsonSerializer IKeyValuePairSerializerV2.KeySerializer => KeySerializer; + + /// + /// Gets the value serializer. + /// + IBsonSerializer IKeyValuePairSerializerV2.ValueSerializer => ValueSerializer; + // public methods /// /// Deserializes a value. diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs index bf729df32a9..13c9fc2a157 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs @@ -134,6 +134,11 @@ public static AstExpression ArrayElemAt(AstExpression array, AstExpression index return new AstBinaryExpression(AstBinaryOperator.ArrayElemAt, array, index); } + public static AstExpression ArrayToObject(AstExpression arg) + { + return new AstUnaryExpression(AstUnaryOperator.ArrayToObject, arg); + } + public static AstExpression Avg(AstExpression array) { return new AstUnaryExpression(AstUnaryOperator.Avg, array); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs index 58cf7f6ab61..6b1251cb5dc 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs @@ -14,7 +14,6 @@ */ using System; -using System.Collections; using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; @@ -66,11 +65,21 @@ public static TranslatedExpression Translate(TranslationContext context, MemberE if (!DocumentSerializerHelper.AreMembersRepresentedAsFields(containerTranslation.Serializer, out _)) { - if (member is PropertyInfo propertyInfo && propertyInfo.Name == "Length") + if (member is PropertyInfo propertyInfo && propertyInfo.Name == "Length") { return LengthPropertyToAggregationExpressionTranslator.Translate(context, expression); } + if (TryTranslateDictionaryProperty(expression, containerTranslation, member, out var translatedDictionaryProperty)) + { + return translatedDictionaryProperty; + } + + if (TryTranslateKeyValuePairProperty(expression, containerTranslation, member, out var translatedKeyValuePairProperty)) + { + return translatedKeyValuePairProperty; + } + if (TryTranslateCollectionCountProperty(expression, containerTranslation, member, out var translatedCount)) { return translatedCount; @@ -121,11 +130,20 @@ private static bool TryTranslateCollectionCountProperty(MemberExpression express { if (EnumerableProperty.IsCountProperty(expression)) { - SerializationHelper.EnsureRepresentationIsArray(expression, container.Serializer); + AstExpression ast; - var ast = AstExpression.Size(container.Ast); - var serializer = Int32Serializer.Instance; + if (container.Serializer is IBsonDictionarySerializer dictionarySerializer && + dictionarySerializer.DictionaryRepresentation == DictionaryRepresentation.Document) + { + ast = AstExpression.Size(AstExpression.ObjectToArray(container.Ast)); + } + else + { + SerializationHelper.EnsureRepresentationIsArray(expression, container.Serializer); + ast = AstExpression.Size(container.Ast); + } + var serializer = Int32Serializer.Instance; result = new TranslatedExpression(expression, ast, serializer); return true; } @@ -187,5 +205,142 @@ private static bool TryTranslateDateTimeProperty(MemberExpression expression, Tr return false; } + + private static bool TryTranslateKeyValuePairProperty(MemberExpression expression, TranslatedExpression container, MemberInfo memberInfo, out TranslatedExpression result) + { + result = null; + + if (container.Expression.Type.IsGenericType && + container.Expression.Type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>) && + container.Serializer is IKeyValuePairSerializerV2 { Representation: BsonType.Array } kvpSerializer) + { + AstExpression ast; + IBsonSerializer serializer; + + switch (memberInfo.Name) + { + case "Key": + ast = AstExpression.ArrayElemAt(container.Ast, 0); + serializer = kvpSerializer.KeySerializer; + break; + case "Value": + ast = AstExpression.ArrayElemAt(container.Ast, 1); + serializer = kvpSerializer.ValueSerializer; + break; + default: + throw new ExpressionNotSupportedException(expression); + } + result = new TranslatedExpression(expression, ast, serializer); + return true; + } + + return false; + } + + private static bool TryTranslateDictionaryProperty(MemberExpression expression, TranslatedExpression container, MemberInfo memberInfo, out TranslatedExpression result) + { + result = null; + + if (memberInfo is PropertyInfo propertyInfo && + propertyInfo.DeclaringType.IsGenericType && + (propertyInfo.DeclaringType.GetGenericTypeDefinition() == typeof(Dictionary<,>) || + propertyInfo.DeclaringType.GetGenericTypeDefinition() == typeof(IDictionary<,>))) + { + if (container.Serializer is IBsonDictionarySerializer dictionarySerializer) + { + switch (propertyInfo.Name) + { + case "Count": + { + AstExpression countAst; + switch (dictionarySerializer.DictionaryRepresentation) + { + case DictionaryRepresentation.Document: + countAst = AstExpression.Size(AstExpression.ObjectToArray(container.Ast)); + break; + case DictionaryRepresentation.ArrayOfArrays: + case DictionaryRepresentation.ArrayOfDocuments: + countAst = AstExpression.Size(container.Ast); + break; + default: + throw new ExpressionNotSupportedException(expression); + } + + var serializer = Int32Serializer.Instance; + result = new TranslatedExpression(expression, countAst, serializer); + return true; + } + + case "Keys": + { + AstExpression keysAst; + switch (dictionarySerializer.DictionaryRepresentation) + { + case DictionaryRepresentation.Document: + keysAst = AstExpression.GetField(AstExpression.ObjectToArray(container.Ast), "k"); + break; + case DictionaryRepresentation.ArrayOfArrays: + { + var kvp = AstExpression.Var("kvp"); + keysAst = AstExpression.Map( + input: container.Ast, + @as: kvp, + @in: AstExpression.ArrayElemAt(kvp, 0)); + break; + } + case DictionaryRepresentation.ArrayOfDocuments: + keysAst = AstExpression.GetField(container.Ast, "k"); + break; + + default: + throw new ExpressionNotSupportedException(expression); + } + + var serializer = ArraySerializerHelper.CreateSerializer(dictionarySerializer.KeySerializer); + result = new TranslatedExpression(expression, keysAst, serializer); + return true; + } + + case "Values": + { + AstExpression valuesAst; + switch (dictionarySerializer.DictionaryRepresentation) + { + case DictionaryRepresentation.Document: + valuesAst = AstExpression.GetField(AstExpression.ObjectToArray(container.Ast), "v"); + break; + case DictionaryRepresentation.ArrayOfArrays: + { + var kvp = AstExpression.Var("kvp"); + valuesAst = AstExpression.Map( + input: container.Ast, + @as: kvp, + @in: AstExpression.ArrayElemAt(kvp, 1)); + break; + } + case DictionaryRepresentation.ArrayOfDocuments: + valuesAst = AstExpression.GetField(container.Ast, "v"); + break; + + default: + throw new ExpressionNotSupportedException(expression); + } + + var serializer = ArraySerializerHelper.CreateSerializer(dictionarySerializer.ValueSerializer); + result = new TranslatedExpression(expression, valuesAst, serializer); + return true; + } + + default: + throw new ExpressionNotSupportedException(expression); + } + } + + throw new ExpressionNotSupportedException(expression, because: "serializer does not implement IBsonDictionarySerializer"); + } + + return false; + } + } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsKeyMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsKeyMethodToAggregationExpressionTranslator.cs index 452a3839fca..07c7135670f 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsKeyMethodToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsKeyMethodToAggregationExpressionTranslator.cs @@ -36,27 +36,65 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC { var dictionaryExpression = expression.Object; var keyExpression = arguments[0]; + return TranslateContainsKey(context, expression, dictionaryExpression, keyExpression); + } + + throw new ExpressionNotSupportedException(expression); + } - var dictionaryTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, dictionaryExpression); - var dictionarySerializer = GetDictionarySerializer(expression, dictionaryTranslation); - var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation; + public static TranslatedExpression TranslateContainsKey(TranslationContext context, Expression expression, Expression dictionaryExpression, Expression keyExpression) + { + var dictionaryTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, dictionaryExpression); + var dictionarySerializer = GetDictionarySerializer(expression, dictionaryTranslation); + var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation; - AstExpression ast; - switch (dictionaryRepresentation) - { - case DictionaryRepresentation.Document: + AstExpression ast; + switch (dictionaryRepresentation) + { + case DictionaryRepresentation.Document: + { var keyFieldName = GetKeyFieldName(context, expression, keyExpression, dictionarySerializer.KeySerializer); ast = AstExpression.IsNotMissing(AstExpression.GetField(dictionaryTranslation.Ast, keyFieldName)); break; + } - default: - throw new ExpressionNotSupportedException(expression, because: $"ContainsKey is not supported when DictionaryRepresentation is: {dictionaryRepresentation}"); - } + case DictionaryRepresentation.ArrayOfDocuments: + { + var keyFieldName = GetKeyFieldName(context, expression, keyExpression, dictionarySerializer.KeySerializer); + var (valueBinding, valueAst) = AstExpression.UseVarIfNotSimple("value", keyFieldName); + ast = AstExpression.Let( + var: valueBinding, + @in: AstExpression.Reduce( + input: dictionaryTranslation.Ast, + initialValue: false, + @in: AstExpression.Cond( + @if: AstExpression.Var("value"), + @then: true, + @else: AstExpression.Eq(AstExpression.GetField(AstExpression.Var("this"), "k"), valueAst)))); + break; + } + + case DictionaryRepresentation.ArrayOfArrays: + { + var keyFieldName = GetKeyFieldName(context, expression, keyExpression, dictionarySerializer.KeySerializer); + var (valueBinding, valueAst) = AstExpression.UseVarIfNotSimple("value", keyFieldName); + ast = AstExpression.Let( + var: valueBinding, + @in: AstExpression.Reduce( + input: dictionaryTranslation.Ast, + initialValue: false, + @in: AstExpression.Cond( + @if: AstExpression.Var("value"), + @then: true, + @else: AstExpression.Eq(AstExpression.ArrayElemAt(AstExpression.Var("this"), 0), valueAst)))); + break; + } - return new TranslatedExpression(expression, ast, BooleanSerializer.Instance); + default: + throw new ExpressionNotSupportedException(expression, because: $"DictionaryRepresentation: {dictionaryRepresentation} is not supported."); } - throw new ExpressionNotSupportedException(expression); + return new TranslatedExpression(expression, ast, BooleanSerializer.Instance); } private static AstExpression GetKeyFieldName(TranslationContext context, Expression expression, Expression keyExpression, IBsonSerializer keySerializer) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsMethodToAggregationExpressionTranslator.cs index 7a4d64a3ff0..31b9255a606 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsMethodToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsMethodToAggregationExpressionTranslator.cs @@ -13,6 +13,7 @@ * limitations under the License. */ +using System.Collections.Generic; using System.Linq.Expressions; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; @@ -33,6 +34,11 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC if (IsEnumerableContainsMethod(expression, out var sourceExpression, out var valueExpression)) { + if (TryTranslateDictionaryKeysOrValuesContains(context, expression, sourceExpression, valueExpression, out var dictionaryTranslation)) + { + return dictionaryTranslation; + } + return TranslateEnumerableContains(context, expression, sourceExpression, valueExpression); } @@ -83,5 +89,48 @@ private static TranslatedExpression TranslateEnumerableContains(TranslationConte return new TranslatedExpression(expression, ast, BooleanSerializer.Instance); } + + private static bool TryTranslateDictionaryKeysOrValuesContains( + TranslationContext context, + Expression expression, + Expression sourceExpression, + Expression valueExpression, + out TranslatedExpression translation) + { + translation = null; + + if (sourceExpression is not MemberExpression memberExpression) + { + return false; + } + + var memberName = memberExpression.Member.Name; + var declaringType = memberExpression.Member.DeclaringType; + + if (!declaringType.IsGenericType || + (declaringType.GetGenericTypeDefinition() != typeof(Dictionary<,>) && + declaringType.GetGenericTypeDefinition() != typeof(IDictionary<,>))) + { + return false; + } + + switch (memberName) + { + case "Keys": + { + var dictionaryExpression = memberExpression.Expression; + translation = ContainsKeyMethodToAggregationExpressionTranslator.TranslateContainsKey(context, expression, dictionaryExpression, valueExpression); + return true; + } + case "Values": + { + var dictionaryExpression = memberExpression.Expression; + translation = ContainsValueMethodToAggregationExpressionTranslator.TranslateContainsValue(context, expression, dictionaryExpression, valueExpression); + return true; + } + default: + return false; + } + } } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsValueMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsValueMethodToAggregationExpressionTranslator.cs index d3545e44266..407413c8ff3 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsValueMethodToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsValueMethodToAggregationExpressionTranslator.cs @@ -34,61 +34,65 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC { var dictionaryExpression = expression.Object; var valueExpression = arguments[0]; + return TranslateContainsValue(context, expression, dictionaryExpression, valueExpression); + } - var dictionaryTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, dictionaryExpression); - var dictionarySerializer = GetDictionarySerializer(expression, dictionaryTranslation); - var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation; + throw new ExpressionNotSupportedException(expression); + } - var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression); - var (valueBinding, valueAst) = AstExpression.UseVarIfNotSimple("value", valueTranslation.Ast); + public static TranslatedExpression TranslateContainsValue(TranslationContext context, Expression expression, Expression dictionaryExpression, Expression valueExpression) + { + var dictionaryTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, dictionaryExpression); + var dictionarySerializer = GetDictionarySerializer(expression, dictionaryTranslation); + var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation; - AstExpression ast; - switch (dictionaryRepresentation) - { - case DictionaryRepresentation.Document: - ast = AstExpression.Let( - var: valueBinding, - @in: AstExpression.Reduce( - input: AstExpression.ObjectToArray(dictionaryTranslation.Ast), - initialValue: false, - @in: AstExpression.Cond( - @if: AstExpression.Var("value"), - @then: true, - @else: AstExpression.Eq(AstExpression.GetField(AstExpression.Var("this"), "v"), valueAst)))); - break; + var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression); + var (valueBinding, valueAst) = AstExpression.UseVarIfNotSimple("value", valueTranslation.Ast); - case DictionaryRepresentation.ArrayOfArrays: - ast = AstExpression.Let( - var: valueBinding, - @in: AstExpression.Reduce( - input: dictionaryTranslation.Ast, - initialValue: false, - @in: AstExpression.Cond( - @if: AstExpression.Var("value"), - @then: true, - @else: AstExpression.Eq(AstExpression.ArrayElemAt(AstExpression.Var("this"), 1), valueAst)))); - break; + AstExpression ast; + switch (dictionaryRepresentation) + { + case DictionaryRepresentation.Document: + ast = AstExpression.Let( + var: valueBinding, + @in: AstExpression.Reduce( + input: AstExpression.ObjectToArray(dictionaryTranslation.Ast), + initialValue: false, + @in: AstExpression.Cond( + @if: AstExpression.Var("value"), + @then: true, + @else: AstExpression.Eq(AstExpression.GetField(AstExpression.Var("this"), "v"), valueAst)))); + break; - case DictionaryRepresentation.ArrayOfDocuments: - ast = AstExpression.Let( - var: valueBinding, - @in: AstExpression.Reduce( - input: dictionaryTranslation.Ast, - initialValue: false, - @in: AstExpression.Cond( - @if: AstExpression.Var("value"), - @then: true, - @else: AstExpression.Eq(AstExpression.GetField(AstExpression.Var("this"), "v"), valueAst)))); - break; + case DictionaryRepresentation.ArrayOfArrays: + ast = AstExpression.Let( + var: valueBinding, + @in: AstExpression.Reduce( + input: dictionaryTranslation.Ast, + initialValue: false, + @in: AstExpression.Cond( + @if: AstExpression.Var("value"), + @then: true, + @else: AstExpression.Eq(AstExpression.ArrayElemAt(AstExpression.Var("this"), 1), valueAst)))); + break; - default: - throw new ExpressionNotSupportedException(expression, because: $"ContainsValue is not supported when DictionaryRepresentation is: {dictionaryRepresentation}"); - } + case DictionaryRepresentation.ArrayOfDocuments: + ast = AstExpression.Let( + var: valueBinding, + @in: AstExpression.Reduce( + input: dictionaryTranslation.Ast, + initialValue: false, + @in: AstExpression.Cond( + @if: AstExpression.Var("value"), + @then: true, + @else: AstExpression.Eq(AstExpression.GetField(AstExpression.Var("this"), "v"), valueAst)))); + break; - return new TranslatedExpression(expression, ast, BooleanSerializer.Instance); + default: + throw new ExpressionNotSupportedException(expression, because: $"DictionaryRepresentation: {dictionaryRepresentation} is not supported."); } - throw new ExpressionNotSupportedException(expression); + return new TranslatedExpression(expression, ast, BooleanSerializer.Instance); } private static IBsonDictionarySerializer GetDictionarySerializer(Expression expression, TranslatedExpression dictionaryTranslation) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/GetItemMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/GetItemMethodToAggregationExpressionTranslator.cs index 88bf49554f6..41e75b1620f 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/GetItemMethodToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/GetItemMethodToAggregationExpressionTranslator.cs @@ -119,10 +119,6 @@ private static TranslatedExpression TranslateIDictionaryGetItemWithKey(Translati { throw new ExpressionNotSupportedException(expression, because: $"dictionary serializer class {dictionaryTranslation.Serializer.GetType()} does not implement {nameof(IBsonDictionarySerializer)}"); } - if (dictionarySerializer.DictionaryRepresentation != DictionaryRepresentation.Document) - { - throw new ExpressionNotSupportedException(expression, because: "dictionary is not represented as a document"); - } var keySerializer = dictionarySerializer.KeySerializer; AstExpression keyFieldNameAst; @@ -159,7 +155,27 @@ private static TranslatedExpression TranslateIDictionaryGetItemWithKey(Translati keyFieldNameAst = keyTranslation.Ast; } - var ast = AstExpression.GetField(dictionaryTranslation.Ast, keyFieldNameAst); + var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation; + AstExpression ast; + switch (dictionaryRepresentation) + { + case DictionaryRepresentation.Document: + ast = AstExpression.GetField(dictionaryTranslation.Ast, keyFieldNameAst); + break; + + case DictionaryRepresentation.ArrayOfArrays: + ast = AstExpression.GetField(AstExpression.ArrayToObject(dictionaryTranslation.Ast), keyFieldNameAst); + break; + + case DictionaryRepresentation.ArrayOfDocuments: + var filter = AstExpression.Filter(dictionaryTranslation.Ast, + AstExpression.Eq(AstExpression.GetField(AstExpression.Var("kvp"), "k"), keyFieldNameAst), "kvp"); + ast = AstExpression.GetField(AstExpression.ArrayElemAt(filter, 0), "v"); + break; + default: + throw new ExpressionNotSupportedException(expression, because: $"Indexer access is not supported when DictionaryRepresentation is: {dictionaryRepresentation}"); + } + return new TranslatedExpression(expression, ast, dictionarySerializer.ValueSerializer); } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/AllOrAnyMethodToFilterTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/AllOrAnyMethodToFilterTranslator.cs index abb59233f3c..74875b1c5e5 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/AllOrAnyMethodToFilterTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/AllOrAnyMethodToFilterTranslator.cs @@ -13,13 +13,12 @@ * limitations under the License. */ -using System; using System.Linq; using System.Linq.Expressions; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; -using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Bson.Serialization.Options; using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters; using MongoDB.Driver.Linq.Linq3Implementation.Misc; using MongoDB.Driver.Linq.Linq3Implementation.Reflection; @@ -50,6 +49,11 @@ public static AstFilter Translate(TranslationContext context, MethodCallExpressi var sourceExpression = method.IsStatic ? arguments[0] : expression.Object; var (fieldTranslation, filter) = FilteredEnumerableFilterFieldTranslator.Translate(context, sourceExpression); + if (fieldTranslation.Serializer is IBsonDictionarySerializer { DictionaryRepresentation: DictionaryRepresentation.Document }) + { + throw new ExpressionNotSupportedException(expression); + } + if (method.IsOneOf(EnumerableMethod.All, EnumerableMethod.AnyWithPredicate, ArrayMethod.Exists) || ListMethod.IsExistsMethod(method)) { var predicateLambda = (LambdaExpression)(method.IsStatic ? arguments[1] : arguments[0]); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsKeyMethodToFilterTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsKeyMethodToFilterTranslator.cs index 48398cec982..12233a70697 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsKeyMethodToFilterTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsKeyMethodToFilterTranslator.cs @@ -35,24 +35,40 @@ public static AstFilter Translate(TranslationContext context, MethodCallExpressi { var fieldExpression = expression.Object; var keyExpression = arguments[0]; + return TranslateContainsKey(context, expression, fieldExpression, keyExpression); + } + + throw new ExpressionNotSupportedException(expression); + } - var fieldTranslation = ExpressionToFilterFieldTranslator.Translate(context, fieldExpression); - var dictionarySerializer = GetDictionarySerializer(expression, fieldTranslation); - var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation; + public static AstFilter TranslateContainsKey(TranslationContext context, Expression expression, Expression fieldExpression, Expression keyExpression) + { + var fieldTranslation = ExpressionToFilterFieldTranslator.Translate(context, fieldExpression); + var dictionarySerializer = GetDictionarySerializer(expression, fieldTranslation); + var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation; - switch (dictionaryRepresentation) - { - case DictionaryRepresentation.Document: + switch (dictionaryRepresentation) + { + case DictionaryRepresentation.Document: + { var key = GetKeyStringConstant(expression, keyExpression, dictionarySerializer.KeySerializer); var keyField = fieldTranslation.Ast.SubField(key); return AstFilter.Exists(keyField); + } - default: - throw new ExpressionNotSupportedException(expression, because: $"ContainsKey is not supported when DictionaryRepresentation is: {dictionaryRepresentation}"); - } - } + case DictionaryRepresentation.ArrayOfDocuments: + case DictionaryRepresentation.ArrayOfArrays: + { + var key = GetKeyStringConstant(expression, keyExpression, dictionarySerializer.KeySerializer); + var fieldName = dictionaryRepresentation == DictionaryRepresentation.ArrayOfDocuments ? "k" : "0"; + var keyField = AstFilter.Field(fieldName); + var keyMatchFilter = AstFilter.Eq(keyField, key); + return AstFilter.ElemMatch(fieldTranslation.Ast, keyMatchFilter); + } - throw new ExpressionNotSupportedException(expression); + default: + throw new ExpressionNotSupportedException(expression, because: $"DictionaryRepresentation: {dictionaryRepresentation} is not supported for ContainsKey method."); + } } private static IBsonDictionarySerializer GetDictionarySerializer(Expression expression, TranslatedFilterField field) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsMethodToFilterTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsMethodToFilterTranslator.cs index 574a93809c6..abcdf87c4e6 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsMethodToFilterTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsMethodToFilterTranslator.cs @@ -16,7 +16,9 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Linq.Expressions; +using MongoDB.Bson.Serialization; using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters; using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods; using MongoDB.Driver.Linq.Linq3Implementation.Misc; @@ -45,6 +47,12 @@ public static AstFilter Translate(TranslationContext context, MethodCallExpressi var fieldType = fieldExpression.Type; var itemExpression = arguments[1]; var itemType = itemExpression.Type; + + if (TryTranslateDictionaryKeysOrValuesContains(context, expression, fieldExpression, itemExpression, out var dictionaryFilter)) + { + return dictionaryFilter; + } + if (TypeImplementsIEnumerable(fieldType, itemType)) { return Translate(context, expression, fieldExpression, itemExpression); @@ -57,8 +65,15 @@ public static AstFilter Translate(TranslationContext context, MethodCallExpressi arguments.Count == 1) { var fieldExpression = expression.Object; - var fieldType = fieldExpression.Type; var itemExpression = arguments[0]; + + if (TryTranslateDictionaryKeysOrValuesContains(context, expression, fieldExpression, itemExpression, out var dictionaryFilter)) + { + return dictionaryFilter; + } + + // Otherwise, handle as regular Contains on IEnumerable + var fieldType = fieldExpression.Type; var itemType = itemExpression.Type; if (TypeImplementsIEnumerable(fieldType, itemType)) { @@ -91,5 +106,48 @@ private static bool TypeImplementsIEnumerable(Type type, Type itemType) var ienumerableType = typeof(IEnumerable<>).MakeGenericType(itemType); return ienumerableType.IsAssignableFrom(type); } + + private static bool TryTranslateDictionaryKeysOrValuesContains( + TranslationContext context, + Expression expression, + Expression fieldExpression, + Expression itemExpression, + out AstFilter filter) + { + filter = null; + + if (fieldExpression is not MemberExpression memberExpression) + { + return false; + } + + var memberName = memberExpression.Member.Name; + var declaringType = memberExpression.Member.DeclaringType; + + if (!declaringType.IsGenericType || + (declaringType.GetGenericTypeDefinition() != typeof(Dictionary<,>) && + declaringType.GetGenericTypeDefinition() != typeof(IDictionary<,>))) + { + return false; + } + + switch (memberName) + { + case "Keys": + { + var dictionaryExpression = memberExpression.Expression; + filter = ContainsKeyMethodToFilterTranslator.TranslateContainsKey(context, expression, dictionaryExpression, itemExpression); + return true; + } + case "Values": + { + var dictionaryExpression = memberExpression.Expression; + filter = ContainsValueMethodToFilterTranslator.TranslateContainsValue(context, expression, dictionaryExpression, itemExpression); + return true; + } + default: + return false; + } + } } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsValueMethodToFilterTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsValueMethodToFilterTranslator.cs index 2066d8f789a..3d863cb91bc 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsValueMethodToFilterTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/ContainsValueMethodToFilterTranslator.cs @@ -34,26 +34,36 @@ public static AstFilter Translate(TranslationContext context, MethodCallExpressi { var fieldExpression = expression.Object; var valueExpression = arguments[0]; + return TranslateContainsValue(context, expression, fieldExpression, valueExpression); + } - var fieldTranslation = ExpressionToFilterFieldTranslator.Translate(context, fieldExpression); - var dictionarySerializer = GetDictionarySerializer(expression, fieldTranslation); - var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation; - var valueSerializer = dictionarySerializer.ValueSerializer; + throw new ExpressionNotSupportedException(expression); + } - if (valueExpression is ConstantExpression constantValueExpression) - { - var valueField = AstFilter.Field("v"); - var value = constantValueExpression.Value; - var serializedValue = SerializationHelper.SerializeValue(valueSerializer, value); + public static AstFilter TranslateContainsValue(TranslationContext context, Expression expression, Expression fieldExpression, Expression valueExpression) + { + var fieldTranslation = ExpressionToFilterFieldTranslator.Translate(context, fieldExpression); + var dictionarySerializer = GetDictionarySerializer(expression, fieldTranslation); + var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation; + var valueSerializer = dictionarySerializer.ValueSerializer; + + if (valueExpression is ConstantExpression constantValueExpression) + { + var value = constantValueExpression.Value; + var serializedValue = SerializationHelper.SerializeValue(valueSerializer, value); - switch (dictionaryRepresentation) - { - case DictionaryRepresentation.ArrayOfDocuments: + switch (dictionaryRepresentation) + { + case DictionaryRepresentation.ArrayOfDocuments: + case DictionaryRepresentation.ArrayOfArrays: + { + var fieldName = dictionaryRepresentation == DictionaryRepresentation.ArrayOfDocuments ? "v" : "1"; + var valueField = AstFilter.Field(fieldName); return AstFilter.ElemMatch(fieldTranslation.Ast, AstFilter.Eq(valueField, serializedValue)); + } - default: - throw new ExpressionNotSupportedException(expression, because: $"ContainsValue is not supported when DictionaryRepresentation is: {dictionaryRepresentation}"); - } + default: + throw new ExpressionNotSupportedException(expression, because: $"DictionaryRepresentation: {dictionaryRepresentation} is not supported for ContainsValue method."); } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/ToFilterFieldTranslators/MemberExpressionToFilterFieldTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/ToFilterFieldTranslators/MemberExpressionToFilterFieldTranslator.cs index bbeba78f6ce..f073f2c3edf 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/ToFilterFieldTranslators/MemberExpressionToFilterFieldTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/ToFilterFieldTranslators/MemberExpressionToFilterFieldTranslator.cs @@ -14,11 +14,11 @@ */ using System; +using System.Collections.Generic; using System.Linq.Expressions; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; -using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters; using MongoDB.Driver.Linq.Linq3Implementation.Misc; namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ToFilterFieldTranslators @@ -42,6 +42,13 @@ public static TranslatedFilterField Translate(TranslationContext context, Member var fieldSerializer = fieldTranslation.Serializer; var fieldSerializerType = fieldSerializer.GetType(); + if (memberExpression.Type.IsGenericType && + (memberExpression.Type.GetGenericTypeDefinition() == typeof(Dictionary<,>.KeyCollection) || + memberExpression.Type.GetGenericTypeDefinition() == typeof(Dictionary<,>.ValueCollection))) + { + throw new ExpressionNotSupportedException(memberExpression); + } + if (fieldSerializer.GetType() == typeof(BsonValueSerializer)) { var field = fieldTranslation.Ast; @@ -134,6 +141,28 @@ public static TranslatedFilterField Translate(TranslationContext context, Member throw new ExpressionNotSupportedException(memberExpression, because: $"serializer {fieldTranslation.Serializer.GetType().FullName} does not implement IBsonTupleSerializer"); } + if (fieldExpression.Type.IsGenericType && + fieldExpression.Type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>) && + fieldSerializer is IKeyValuePairSerializerV2 keyValuePairSerializer) + { + switch (memberExpression.Member.Name) + { + case "Key": + if (keyValuePairSerializer.Representation == BsonType.Array) + { + return fieldTranslation.SubField("0", keyValuePairSerializer.KeySerializer); + } + return fieldTranslation.SubField("k", keyValuePairSerializer.KeySerializer); + + case "Value": + if (keyValuePairSerializer.Representation == BsonType.Array) + { + return fieldTranslation.SubField("1", keyValuePairSerializer.ValueSerializer); + } + return fieldTranslation.SubField("v", keyValuePairSerializer.ValueSerializer); + } + } + throw new ExpressionNotSupportedException(memberExpression); } } From 34f74cedc65f8ebbd478587ea032f9cc21a5913b Mon Sep 17 00:00:00 2001 From: adelinowona Date: Thu, 30 Oct 2025 20:39:34 -0400 Subject: [PATCH 2/2] add tests and update/remove duplicated tests --- .../Jira/CSharp2509Tests.cs | 163 - .../Jira/CSharp4443Tests.cs | 2800 +++++++++++++++++ .../Jira/CSharp4557Tests.cs | 78 - .../Jira/CSharp4813Tests.cs | 222 -- 4 files changed, 2800 insertions(+), 463 deletions(-) delete mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp2509Tests.cs create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs delete mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4557Tests.cs diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp2509Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp2509Tests.cs deleted file mode 100644 index b64e62ed650..00000000000 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp2509Tests.cs +++ /dev/null @@ -1,163 +0,0 @@ -/* 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 System.Linq; -using FluentAssertions; -using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Bson.Serialization.Options; -using MongoDB.Driver.TestHelpers; -using Xunit; - -namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira -{ - public class CSharp2509Tests : LinqIntegrationTest - { - public CSharp2509Tests(ClassFixture fixture) - : base(fixture) - { - } - - [Fact] - public void Where_ContainsValue_should_work_when_representation_is_Dictionary() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Where(x => x.D1.ContainsValue(1)); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $match : { $expr : { $reduce : { input : { $objectToArray : '$D1' }, initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 1] } } } } } } }"); - - var results = queryable.ToList(); - results.Select(x => x.Id).Should().Equal(1, 2); - } - - [Fact] - public void Where_ContainsValue_should_work_when_representation_is_ArrayOfArrays() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Where(x => x.D2.ContainsValue(1)); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $match : { $expr : { $reduce : { input : '$D2', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : [{ $arrayElemAt : ['$$this', 1] }, 1] } } } } } } }"); - - var results = queryable.ToList(); - results.Select(x => x.Id).Should().Equal(1, 2); - } - - [Fact] - public void Where_ContainsValue_should_work_when_representation_is_ArrayOfDocuments() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Where(x => x.D3.ContainsValue(1)); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $match : { D3 : { $elemMatch : { v : 1 } } } }"); - - var results = queryable.ToList(); - results.Select(x => x.Id).Should().Equal(1, 2); - } - - [Fact] - public void Select_ContainsValue_should_work_when_representation_is_Dictionary() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Select(x => x.D1.ContainsValue(1)); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $project : { _v : { $reduce : { input : { $objectToArray : '$D1' }, initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 1] } } } } }, _id : 0 } }"); - - var results = queryable.ToList(); - results.Should().Equal(true, true, false); - } - - [Fact] - public void Select_ContainsValue_should_work_when_representation_is_ArrayOfArrays() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Select(x => x.D2.ContainsValue(1)); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$D2', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : [{ $arrayElemAt : ['$$this', 1] }, 1] } } } } }, _id : 0 } }"); - - var results = queryable.ToList(); - results.Should().Equal(true, true, false); - } - - [Fact] - public void Select_ContainsValue_should_work_when_representation_is_ArrayOfDocuments() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Select(x => x.D3.ContainsValue(1)); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$D3', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 1] } } } } }, _id : 0 } }"); - - var results = queryable.ToList(); - results.Should().Equal(true, true, false); - } - - public class User - { - public int Id { get; set; } - [BsonDictionaryOptions(DictionaryRepresentation.Document)] - public Dictionary D1 { get; set; } - [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] - public Dictionary D2 { get; set; } - [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] - public Dictionary D3 { get; set; } - } - - public sealed class ClassFixture : MongoCollectionFixture - { - protected override IEnumerable InitialData => - [ - new User - { - Id = 1, - D1 = new() { { "A", 1 }, { "B", 2 } }, - D2 = new() { { "A", 1 }, { "B", 2 } }, - D3 = new() { { "A", 1 }, { "B", 2 } } - }, - new User - { - Id = 2, - D1 = new() { { "A", 2 }, { "B", 1 } }, - D2 = new() { { "A", 2 }, { "B", 1 } }, - D3 = new() { { "A", 2 }, { "B", 1 } } - }, - new User - { - Id = 3, - D1 = new() { { "A", 2 }, { "B", 3 } }, - D2 = new() { { "A", 2 }, { "B", 3 } }, - D3 = new() { { "A", 2 }, { "B", 3 } } - } - ]; - } - } -} diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs new file mode 100644 index 00000000000..b5b0e81790b --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs @@ -0,0 +1,2800 @@ +/* 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 FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson.Serialization.Options; +using MongoDB.Driver.Linq; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira; + +public class CSharp4443Tests : LinqIntegrationTest +{ + public CSharp4443Tests(ClassFixture fixture) + : base(fixture) + { + } + + [Fact] + public void Projecting_dictionary_keys_with_arrayOfArrays_should_throw() + { + var exception = Record.Exception(() => + { + _ = Fixture.ArrayOfArraysCollection.AsQueryable() + .Select(x => x.Dictionary.Keys) + .ToList(); + }); + + exception.Should().BeOfType(); + } + + [Fact] + public void Projecting_dictionary_keys_with_arrayOfDocs_should_throw() + { + var exception = Record.Exception(() => + { + _ = Fixture.ArrayOfDocsCollection.AsQueryable() + .Select(x => x.Dictionary.Keys) + .ToList(); + }); + + exception.Should().BeOfType(); + } + + [Fact] + public void Projecting_dictionary_keys_with_document_should_throw() + { + var exception = Record.Exception(() => + { + _ = Fixture.DocCollection.AsQueryable() + .Select(x => x.Dictionary.Keys) + .ToList(); + }); + + exception.Should().BeOfType(); + } + + [Fact] + public void Projecting_dictionary_values_with_arrayOfArrays_should_throw() + { + var exception = Record.Exception(() => + { + _ = Fixture.ArrayOfArraysCollection.AsQueryable() + .Select(x => x.Dictionary.Values) + .ToList(); + }); + + exception.Should().BeOfType(); + } + + [Fact] + public void Projecting_dictionary_values_with_arrayOfDocs_should_throw() + { + var exception = Record.Exception(() => + { + _ = Fixture.ArrayOfDocsCollection.AsQueryable() + .Select(x => x.Dictionary.Values) + .ToList(); + }); + + exception.Should().BeOfType(); + } + + [Fact] + public void Projecting_dictionary_values_with_document_should_throw() + { + var exception = Record.Exception(() => + { + _ = Fixture.DocCollection.AsQueryable() + .Select(x => x.Dictionary.Values) + .ToList(); + }); + + exception.Should().BeOfType(); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_All_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $allElementsTrue : { $map : { input : '$Dictionary', as : 'kvp', in : { $gt : [{ $arrayElemAt : ['$$kvp', 1] }, 100] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(false, false, false, true); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_Any_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $anyElementTrue : { $map : { input : '$Dictionary', as : 'kvp', in : { $gt : [{ $arrayElemAt : ['$$kvp', 1] }, 90] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, true, true); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_Average_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Values.Average()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $avg : { $map : { input : '$Dictionary', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(55.666666666666664, 52.0, 67.5, 165.0); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_ContainsKey_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$Dictionary', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : [{ $arrayElemAt : ['$$this', 0] }, 'life'] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_ContainsValue_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.ContainsValue(25)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$Dictionary', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : [{ $arrayElemAt : ['$$this', 1] }, 25] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, false, false); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_Count_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Count); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $size : '$Dictionary' }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(3, 3, 2, 2); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_CountWithPredicate_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Count(kvp => kvp.Value < 50)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : '$Dictionary', as : 'kvp', in : { $cond : { if : { $lt : [{ $arrayElemAt : ['$$kvp', 1] }, 50] }, then : 1, else : 0 } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2, 2, 1, 0); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_First_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.First(kvp => kvp.Key == "age").Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $arrayElemAt : [{ $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : [{ $arrayElemAt : ['$$kvp', 0] }, 'age'] } } }, 0] }, 1] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_FirstOrDefault_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.FirstOrDefault(kvp => kvp.Key.StartsWith("l")).Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $let : { vars : { values : { $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : [{ $indexOfCP : [{ $arrayElemAt : ['$$kvp', 0] }, 'l'] }, 0] } } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : [null, 0], else : { $arrayElemAt : ['$$values', 0] } } } } }, 1] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(42, 41, 0, 0); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_IndexerAccess_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayToObject : '$Dictionary' } }, in : '$$this.age' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_KeysContains_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$Dictionary', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : [{ $arrayElemAt : ['$$this', 0] }, 'life'] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_Max_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Values.Max()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $max : { $map : { input : '$Dictionary', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(100, 85, 100, 200); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_Select_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Select(kvp => kvp.Value).Sum()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : '$Dictionary', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_Sum_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Sum(kvp => kvp.Value)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : '$Dictionary', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_ValuesContains_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$Dictionary', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : [{ $arrayElemAt : ['$$this', 1] }, 42] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, false, false); + } + + [Fact] + public void Select_DictionaryAsArrayOfArrays_Where_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Where(kvp => kvp.Value == 35).Any()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $gt : [{ $size : { $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : [{ $arrayElemAt : ['$$kvp', 1] }, 35] } } } }, 0] }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().Equal(false, false, true, false); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_All_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $allElementsTrue : { $map : { input : '$Dictionary', as : 'kvp', in : { $gt : ['$$kvp.v', 100] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(false, false, false, true); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_Any_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $anyElementTrue : { $map : { input : '$Dictionary', as : 'kvp', in : { $gt : ['$$kvp.v', 90] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, true, true); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_Average_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Values.Average()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $avg : '$Dictionary.v' }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(55.666666666666664, 52.0, 67.5, 165.0); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_ContainsKey_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$Dictionary', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.k', 'life'] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_ContainsValue_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.ContainsValue(25)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$Dictionary', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 25] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, false, false); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_Count_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Count); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $size : '$Dictionary' }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(3, 3, 2, 2); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_CountWithPredicate_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Count(kvp => kvp.Value < 50)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : '$Dictionary', as : 'kvp', in : { $cond : { if : { $lt : ['$$kvp.v', 50] }, then : 1, else : 0 } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2, 2, 1, 0); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_First_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.First(kvp => kvp.Key == "age").Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : ['$$kvp.k', 'age'] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_FirstOrDefault_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.FirstOrDefault(kvp => kvp.Key.StartsWith("l")).Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $let : { vars : { values : { $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : [{ $indexOfCP : ['$$kvp.k', 'l'] }, 0] } } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : { k : null, v : 0 }, else : { $arrayElemAt : ['$$values', 0] } } } } } }, in : '$$this.v' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(42, 41, 0, 0); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_IndexerAccess_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : ['$$kvp.k', 'age'] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_KeysContains_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$Dictionary', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.k', 'life'] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_Max_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Values.Max()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $max : '$Dictionary.v' }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(100, 85, 100, 200); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_Select_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Select(kvp => kvp.Value).Sum()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : '$Dictionary.v' }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_Sum_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Sum(kvp => kvp.Value)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : '$Dictionary.v' }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_ValuesContains_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$Dictionary', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 42] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, false, false); + } + + [Fact] + public void Select_DictionaryAsArrayOfDocuments_Where_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Where(kvp => kvp.Value == 35).Any()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $gt : [{ $size : { $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : ['$$kvp.v', 35] } } } }, 0] }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().Equal(false, false, true, false); + } + + [Fact] + public void Select_DictionaryAsDocument_All_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $allElementsTrue : { $map : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', in : { $gt : ['$$kvp.v', 100] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(false, false, false, true); + } + + [Fact] + public void Select_DictionaryAsDocument_Any_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $anyElementTrue : { $map : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', in : { $gt : ['$$kvp.v', 90] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, true, true); + } + + [Fact] + public void Select_DictionaryAsDocument_Average_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Values.Average()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $avg : { $let : { vars : { this : { $objectToArray : '$Dictionary' } }, in : '$$this.v' } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(55.666666666666664, 52.0, 67.5, 165.0); + } + + [Fact] + public void Select_DictionaryAsDocument_ContainsKey_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $ne : [{ $type : '$Dictionary.life' }, 'missing'] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_DictionaryAsDocument_ContainsValue_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.ContainsValue(25)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : { $objectToArray : '$Dictionary' }, initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 25] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, false, false); + } + + [Fact] + public void Select_DictionaryAsDocument_Count_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Count); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $size : { $objectToArray : '$Dictionary' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(3, 3, 2, 2); + } + + [Fact] + public void Select_DictionaryAsDocument_CountWithPredicate_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Count(kvp => kvp.Value < 50)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', in : { $cond : { if : { $lt : ['$$kvp.v', 50] }, then : 1, else : 0 } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2, 2, 1, 0); + } + + [Fact] + public void Select_DictionaryAsDocument_First_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.First(kvp => kvp.Key == "age").Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', cond : { $eq : ['$$kvp.k', 'age'] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_DictionaryAsDocument_FirstOrDefault_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.FirstOrDefault(kvp => kvp.Key.StartsWith("l")).Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $let : { vars : { values : { $filter : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', cond : { $eq : [{ $indexOfCP : ['$$kvp.k', 'l'] }, 0] } } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : { k : null, v : 0 }, else : { $arrayElemAt : ['$$values', 0] } } } } } }, in : '$$this.v' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(42, 41, 0, 0); + } + + [Fact] + public void Select_DictionaryAsDocument_IndexerAccess_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : '$Dictionary.age', _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_DictionaryAsDocument_KeysContains_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $ne : [{ $type : '$Dictionary.life' }, 'missing'] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_DictionaryAsDocument_Max_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Values.Max()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $max : { $let : { vars : { this : { $objectToArray : '$Dictionary' } }, in : '$$this.v' } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(100, 85, 100, 200); + } + + [Fact] + public void Select_DictionaryAsDocument_Select_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Select(kvp => kvp.Value).Sum()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', in : '$$kvp.v' } } }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_DictionaryAsDocument_Sum_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Sum(kvp => kvp.Value)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', in : '$$kvp.v' } } }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_DictionaryAsDocument_ValuesContains_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : { $objectToArray : '$Dictionary' }, initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 42] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, false, false); + } + + [Fact] + public void Select_DictionaryAsDocument_Where_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.Dictionary.Where(kvp => kvp.Value == 35).Any()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $gt : [{ $size : { $filter : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', cond : { $eq : ['$$kvp.v', 35] } } } }, 0] }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().Equal(false, false, true, false); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_All_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $allElementsTrue : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $gt : [{ $arrayElemAt : ['$$kvp', 1] }, 100] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(false, false, false, true); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_Any_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $anyElementTrue : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $gt : [{ $arrayElemAt : ['$$kvp', 1] }, 90] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, true, true); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_Average_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Values.Average()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $avg : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(55.666666666666664, 52.0, 67.5, 165.0); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_ContainsKey_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$DictionaryInterface', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : [{ $arrayElemAt : ['$$this', 0] }, 'life'] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_Count_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Count); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $size : '$DictionaryInterface' }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(3, 3, 2, 2); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_CountWithPredicate_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Count(kvp => kvp.Value < 50)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $cond : { if : { $lt : [{ $arrayElemAt : ['$$kvp', 1] }, 50] }, then : 1, else : 0 } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2, 2, 1, 0); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_First_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.First(kvp => kvp.Key == "age").Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $arrayElemAt : [{ $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : [{ $arrayElemAt : ['$$kvp', 0] }, 'age'] } } }, 0] }, 1] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_FirstOrDefault_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.FirstOrDefault(kvp => kvp.Key.StartsWith("l")).Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $let : { vars : { values : { $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : [{ $indexOfCP : [{ $arrayElemAt : ['$$kvp', 0] }, 'l'] }, 0] } } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : [null, 0], else : { $arrayElemAt : ['$$values', 0] } } } } }, 1] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(42, 41, 0, 0); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_IndexerAccess_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayToObject : '$DictionaryInterface' } }, in : '$$this.age' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_KeysContains_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$DictionaryInterface', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : [{ $arrayElemAt : ['$$this', 0] }, 'life'] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_Max_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Values.Max()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $max : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(100, 85, 100, 200); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_Select_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Select(kvp => kvp.Value).Sum()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_Sum_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Sum(kvp => kvp.Value)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_ValuesContains_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$DictionaryInterface', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : [{ $arrayElemAt : ['$$this', 1] }, 42] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, false, false); + } + + [Fact] + public void Select_IDictionaryAsArrayOfArrays_Where_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Where(kvp => kvp.Value == 35).Any()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $gt : [{ $size : { $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : [{ $arrayElemAt : ['$$kvp', 1] }, 35] } } } }, 0] }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().Equal(false, false, true, false); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_All_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $allElementsTrue : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $gt : ['$$kvp.v', 100] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(false, false, false, true); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_Any_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $anyElementTrue : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $gt : ['$$kvp.v', 90] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, true, true); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_Average_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Values.Average()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $avg : '$DictionaryInterface.v' }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(55.666666666666664, 52.0, 67.5, 165.0); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_ContainsKey_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$DictionaryInterface', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.k', 'life'] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_Count_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Count); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $size : '$DictionaryInterface' }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(3, 3, 2, 2); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_CountWithPredicate_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Count(kvp => kvp.Value < 50)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $cond : { if : { $lt : ['$$kvp.v', 50] }, then : 1, else : 0 } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2, 2, 1, 0); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_First_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.First(kvp => kvp.Key == "age").Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : ['$$kvp.k', 'age'] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_FirstOrDefault_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.FirstOrDefault(kvp => kvp.Key.StartsWith("l")).Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $let : { vars : { values : { $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : [{ $indexOfCP : ['$$kvp.k', 'l'] }, 0] } } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : { k : null, v : 0 }, else : { $arrayElemAt : ['$$values', 0] } } } } } }, in : '$$this.v' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(42, 41, 0, 0); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_IndexerAccess_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : ['$$kvp.k', 'age'] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_KeysContains_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$DictionaryInterface', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.k', 'life'] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_Max_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Values.Max()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $max : '$DictionaryInterface.v' }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(100, 85, 100, 200); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_Select_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Select(kvp => kvp.Value).Sum()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : '$DictionaryInterface.v' }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_Sum_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Sum(kvp => kvp.Value)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : '$DictionaryInterface.v' }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_ValuesContains_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : '$DictionaryInterface', initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 42] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, false, false); + } + + [Fact] + public void Select_IDictionaryAsArrayOfDocuments_Where_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Where(kvp => kvp.Value == 35).Any()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $gt : [{ $size : { $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : ['$$kvp.v', 35] } } } }, 0] }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().Equal(false, false, true, false); + } + + [Fact] + public void Select_IDictionaryAsDocument_All_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $allElementsTrue : { $map : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', in : { $gt : ['$$kvp.v', 100] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(false, false, false, true); + } + + [Fact] + public void Select_IDictionaryAsDocument_Any_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $anyElementTrue : { $map : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', in : { $gt : ['$$kvp.v', 90] } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, true, true); + } + + [Fact] + public void Select_IDictionaryAsDocument_Average_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Values.Average()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $avg : { $let : { vars : { this : { $objectToArray : '$DictionaryInterface' } }, in : '$$this.v' } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(55.666666666666664, 52.0, 67.5, 165.0); + } + + [Fact] + public void Select_IDictionaryAsDocument_ContainsKey_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $ne : [{ $type : '$DictionaryInterface.life' }, 'missing'] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_IDictionaryAsDocument_Count_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Count); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $size : { $objectToArray : '$DictionaryInterface' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(3, 3, 2, 2); + } + + [Fact] + public void Select_IDictionaryAsDocument_CountWithPredicate_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Count(kvp => kvp.Value < 50)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', in : { $cond : { if : { $lt : ['$$kvp.v', 50] }, then : 1, else : 0 } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(2, 2, 1, 0); + } + + [Fact] + public void Select_IDictionaryAsDocument_First_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.First(kvp => kvp.Key == "age").Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', cond : { $eq : ['$$kvp.k', 'age'] } } }, 0] } }, in : '$$this.v' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_IDictionaryAsDocument_FirstOrDefault_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.FirstOrDefault(kvp => kvp.Key.StartsWith("l")).Value); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $let : { vars : { this : { $let : { vars : { values : { $filter : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', cond : { $eq : [{ $indexOfCP : ['$$kvp.k', 'l'] }, 0] } } } }, in : { $cond : { if : { $eq : [{ $size : '$$values' }, 0] }, then : { k : null, v : 0 }, else : { $arrayElemAt : ['$$values', 0] } } } } } }, in : '$$this.v' } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(42, 41, 0, 0); + } + + [Fact] + public void Select_IDictionaryAsDocument_IndexerAccess_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : '$DictionaryInterface.age', _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(25, 30, 35, 130); + } + + [Fact] + public void Select_IDictionaryAsDocument_KeysContains_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $ne : [{ $type : '$DictionaryInterface.life' }, 'missing'] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false, false); + } + + [Fact] + public void Select_IDictionaryAsDocument_Max_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Values.Max()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $max : { $let : { vars : { this : { $objectToArray : '$DictionaryInterface' } }, in : '$$this.v' } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(100, 85, 100, 200); + } + + [Fact] + public void Select_IDictionaryAsDocument_Select_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Select(kvp => kvp.Value).Sum()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', in : '$$kvp.v' } } }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_IDictionaryAsDocument_Sum_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Sum(kvp => kvp.Value)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $sum : { $map : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', in : '$$kvp.v' } } }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.Should().Equal(167, 156, 135, 330); + } + + [Fact] + public void Select_IDictionaryAsDocument_ValuesContains_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $reduce : { input : { $objectToArray : '$DictionaryInterface' }, initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 42] } } } } }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, false, false); + } + + [Fact] + public void Select_IDictionaryAsDocument_Where_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Select(x => x.DictionaryInterface.Where(kvp => kvp.Value == 35).Any()); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $gt : [{ $size : { $filter : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', cond : { $eq : ['$$kvp.v', 35] } } } }, 0] }, _id : 0 } }"); + + var result = queryable.ToList(); + result.Should().Equal(false, false, true, false); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_All_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $not : { $elemMatch : { '1' : { $not : { $gt : 100 } } } } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle().Which.Name.Should().Be("D"); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_Any_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $elemMatch : { '1' : { $gt : 90 } } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(3); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_ContainsKey_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $elemMatch : { '0' : 'life' } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Should().OnlyContain(doc => doc.Dictionary.ContainsKey("life")); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_ContainsValue_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsValue(25)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $elemMatch : { '1' : 25 } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle() + .Which.Name.Should().Be("A"); + } + + [Theory] + [InlineData(2, 2)] + [InlineData(3, 0)] + public void Where_DictionaryAsArrayOfArrays_Count_should_work(int threshold, int expectedCount) + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Count > threshold); + + var stages = Translate(collection, queryable); + AssertStages(stages, $"{{ $match : {{ 'Dictionary.{threshold}' : {{ $exists : true }} }} }}"); + + var result = queryable.ToList(); + result.Should().HaveCount(expectedCount); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_CountWithPredicate_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Count(kvp => kvp.Value < 50) == 2); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $sum : { $map : { input : '$Dictionary', as : 'kvp', in : { $cond : { if : { $lt : [{ $arrayElemAt : ['$$kvp', 1] }, 50] }, then : 1, else : 0 } } } } }, 2] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_First_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.First(kvp => kvp.Key.StartsWith("l")).Value > 40); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $gt : [{ $arrayElemAt : [{ $arrayElemAt : [{ $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : [{ $indexOfCP : [{ $arrayElemAt : ['$$kvp', 0] }, 'l'] }, 0] } } }, 0] }, 1] }, 40] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Select(x => x.Name).Should().BeEquivalentTo("A", "B"); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_IndexerAccess_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary["life"] == 42); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $let : { vars : { this : { $arrayToObject : '$Dictionary' } }, in : '$$this.life' } }, 42] } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + result.First().Name.Should().Be("A"); + result.First().Dictionary["life"].Should().Be(42); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_KeysContains_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $elemMatch : { '0' : 'life' } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_OrderBy_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsKey("age")) + .OrderBy(x => x.Dictionary["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { Dictionary : { $elemMatch : { '0' : 'age' } } } }", + "{ $project : { _id : 0, _document : '$$ROOT', _key1 : { $let : { vars : { this : { $arrayToObject : '$Dictionary' } }, in : '$$this.age' } } } }", + "{ $sort : { _key1 : 1 } }", + "{ $replaceRoot : { newRoot : '$_document' } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("A"); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_OrderByDescending_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsKey("age")) + .OrderByDescending(x => x.Dictionary["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { Dictionary : { $elemMatch : { '0' : 'age' } } } }", + "{ $project : { _id : 0, _document : '$$ROOT', _key1 : { $let : { vars : { this : { $arrayToObject : '$Dictionary' } }, in : '$$this.age' } } } }", + "{ $sort : { _key1 : -1 } }", + "{ $replaceRoot : { newRoot : '$_document' } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("D"); + } + + [Fact] + public void Where_DictionaryAsArrayOfArrays_ValuesContains_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $elemMatch : { '1' : 42 } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_All_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $not : { $elemMatch : { v : { $not : { $gt : 100 } } } } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle().Which.Name.Should().Be("D"); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_Any_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $elemMatch : { v : { $gt : 90 } } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(3); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_ContainsKey_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $elemMatch : { k : 'life' } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Should().OnlyContain(doc => doc.Dictionary.ContainsKey("life")); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_ContainsValue_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsValue(25)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $elemMatch : { v : 25 } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle() + .Which.Name.Should().Be("A"); + } + + [Theory] + [InlineData(2, 2)] + [InlineData(3, 0)] + public void Where_DictionaryAsArrayOfDocuments_Count_should_work(int threshold, int expectedCount) + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Count > threshold); + + var stages = Translate(collection, queryable); + AssertStages(stages, $"{{ $match : {{ 'Dictionary.{threshold}' : {{ $exists : true }} }} }}"); + + var result = queryable.ToList(); + result.Should().HaveCount(expectedCount); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_CountWithPredicate_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Count(kvp => kvp.Value < 50) == 2); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $sum : { $map : { input : '$Dictionary', as : 'kvp', in : { $cond : { if : { $lt : ['$$kvp.v', 50] }, then : 1, else : 0 } } } } }, 2] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_First_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.First(kvp => kvp.Key.StartsWith("l")).Value > 40); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $gt : [{ $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : [{ $indexOfCP : ['$$kvp.k', 'l'] }, 0] } } }, 0] } }, in : '$$this.v' } }, 40] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Select(x => x.Name).Should().BeEquivalentTo("A", "B"); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_IndexerAccess_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary["life"] == 42); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : ['$$kvp.k', 'life'] } } }, 0] } }, in : '$$this.v' } }, 42] } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + result[0].Name.Should().Be("A"); + result[0].Dictionary["life"].Should().Be(42); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_KeysContains_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $elemMatch : { k : 'life' } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_OrderBy_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsKey("age")) + .OrderBy(x => x.Dictionary["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { Dictionary : { $elemMatch : { k : 'age' } } } }", + "{ $project : { _id : 0, _document : '$$ROOT', _key1 : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : ['$$kvp.k', 'age'] } } }, 0] } }, in : '$$this.v' } } } }", + "{ $sort : { _key1 : 1 } }", + "{ $replaceRoot : { newRoot : '$_document' } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("A"); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_OrderByDescending_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsKey("age")) + .OrderByDescending(x => x.Dictionary["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { Dictionary : { $elemMatch : { k : 'age' } } } }", + "{ $project : { _id : 0, _document : '$$ROOT', _key1 : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$Dictionary', as : 'kvp', cond : { $eq : ['$$kvp.k', 'age'] } } }, 0] } }, in : '$$this.v' } } } }", + "{ $sort : { _key1 : -1 } }", + "{ $replaceRoot : { newRoot : '$_document' } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("D"); + } + + [Fact] + public void Where_DictionaryAsArrayOfDocuments_ValuesContains_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { Dictionary : { $elemMatch : { v : 42 } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + } + + [Fact] + public void Where_DictionaryAsDocument_All_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $allElementsTrue : { $map : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', in : { $gt : ['$$kvp.v', 100] } } } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle().Which.Name.Should().Be("D"); + } + + [Fact] + public void Where_DictionaryAsDocument_Any_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $anyElementTrue : { $map : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', in : { $gt : ['$$kvp.v', 90] } } } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(3); + } + + [Fact] + public void Where_DictionaryAsDocument_ContainsKey_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { 'Dictionary.life' : { $exists : true } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Should().OnlyContain(doc => doc.Dictionary.ContainsKey("life")); + } + + [Fact] + public void Where_DictionaryAsDocument_ContainsValue_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsValue(25)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $reduce : { input : { $objectToArray : '$Dictionary' }, initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 25] } } } } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle() + .Which.Name.Should().Be("A"); + } + + [Theory] + [InlineData(2, 2)] + [InlineData(3, 0)] + public void Where_DictionaryAsDocument_Count_should_work(int threshold, int expectedCount) + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Count > threshold); + + var stages = Translate(collection, queryable); + AssertStages(stages, $"{{ $match : {{ $expr : {{ $gt : [{{ $size : {{ $objectToArray : '$Dictionary' }} }}, {threshold}] }} }} }}"); + + var result = queryable.ToList(); + result.Should().HaveCount(expectedCount); + } + + [Fact] + public void Where_DictionaryAsDocument_CountWithPredicate_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Count(kvp => kvp.Value < 50) == 2); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $sum : { $map : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', in : { $cond : { if : { $lt : ['$$kvp.v', 50] }, then : 1, else : 0 } } } } }, 2] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_DictionaryAsDocument_First_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.First(kvp => kvp.Key.StartsWith("l")).Value > 40); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $gt : [{ $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : { $objectToArray : '$Dictionary' }, as : 'kvp', cond : { $eq : [{ $indexOfCP : ['$$kvp.k', 'l'] }, 0] } } }, 0] } }, in : '$$this.v' } }, 40] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Select(x => x.Name).Should().BeEquivalentTo("A", "B"); + } + + [Fact] + public void Where_DictionaryAsDocument_IndexerAccess_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary["life"] == 42); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { 'Dictionary.life' : 42 } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + result[0].Name.Should().Be("A"); + result[0].Dictionary["life"].Should().Be(42); + } + + [Fact] + public void Where_DictionaryAsDocument_KeysContains_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { 'Dictionary.life' : { $exists : true } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_DictionaryAsDocument_OrderBy_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsKey("age")) + .OrderBy(x => x.Dictionary["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { 'Dictionary.age' : { $exists : true } } }", + "{ $sort : { 'Dictionary.age' : 1 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("A"); + } + + [Fact] + public void Where_DictionaryAsDocument_OrderByDescending_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.ContainsKey("age")) + .OrderByDescending(x => x.Dictionary["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { 'Dictionary.age' : { $exists : true } } }", + "{ $sort : { 'Dictionary.age' : -1 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("D"); + } + + [Fact] + public void Where_DictionaryAsDocument_ValuesContains_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.Dictionary.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $reduce : { input : { $objectToArray : '$Dictionary' }, initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 42] } } } } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_All_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $not : { $elemMatch : { '1' : { $not : { $gt : 100 } } } } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle().Which.Name.Should().Be("D"); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_Any_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $elemMatch : { '1' : { $gt : 90 } } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(3); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_ContainsKey_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $elemMatch : { '0' : 'life' } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Should().OnlyContain(doc => doc.DictionaryInterface.ContainsKey("life")); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_Count_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Count == 3); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $size : 3 } } }"); + + var results = queryable.ToList(); + results.Select(x => x.Name).Should().Equal("A", "B"); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_CountWithPredicate_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Count(kvp => kvp.Value < 50) == 2); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $sum : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $cond : { if : { $lt : [{ $arrayElemAt : ['$$kvp', 1] }, 50] }, then : 1, else : 0 } } } } }, 2] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_First_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.First(kvp => kvp.Key.StartsWith("l")).Value > 40); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $gt : [{ $arrayElemAt : [{ $arrayElemAt : [{ $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : [{ $indexOfCP : [{ $arrayElemAt : ['$$kvp', 0] }, 'l'] }, 0] } } }, 0] }, 1] }, 40] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Select(x => x.Name).Should().BeEquivalentTo("A", "B"); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_IndexerAccess_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface["life"] == 42); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $let : { vars : { this : { $arrayToObject : '$DictionaryInterface' } }, in : '$$this.life' } }, 42] } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + result.First().Name.Should().Be("A"); + result.First().DictionaryInterface["life"].Should().Be(42); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_KeysContains_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $elemMatch : { '0' : 'life' } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_OrderBy_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.ContainsKey("age")) + .OrderBy(x => x.DictionaryInterface["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { DictionaryInterface : { $elemMatch : { '0' : 'age' } } } }", + "{ $project : { _id : 0, _document : '$$ROOT', _key1 : { $let : { vars : { this : { $arrayToObject : '$DictionaryInterface' } }, in : '$$this.age' } } } }", + "{ $sort : { _key1 : 1 } }", + "{ $replaceRoot : { newRoot : '$_document' } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("A"); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_OrderByDescending_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.ContainsKey("age")) + .OrderByDescending(x => x.DictionaryInterface["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { DictionaryInterface : { $elemMatch : { '0' : 'age' } } } }", + "{ $project : { _id : 0, _document : '$$ROOT', _key1 : { $let : { vars : { this : { $arrayToObject : '$DictionaryInterface' } }, in : '$$this.age' } } } }", + "{ $sort : { _key1 : -1 } }", + "{ $replaceRoot : { newRoot : '$_document' } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("D"); + } + + [Fact] + public void Where_IDictionaryAsArrayOfArrays_ValuesContains_should_work() + { + var collection = Fixture.ArrayOfArraysCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $elemMatch : { '1' : 42 } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_All_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $not : { $elemMatch : { v : { $not : { $gt : 100 } } } } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle().Which.Name.Should().Be("D"); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_Any_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $elemMatch : { v : { $gt : 90 } } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(3); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_ContainsKey_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $elemMatch : { k : 'life' } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Should().OnlyContain(doc => doc.DictionaryInterface.ContainsKey("life")); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_Count_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Count == 3); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $size : 3 } } }"); + + var results = queryable.ToList(); + results.Select(x => x.Name).Should().Equal("A", "B"); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_CountWithPredicate_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Count(kvp => kvp.Value < 50) == 2); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $sum : { $map : { input : '$DictionaryInterface', as : 'kvp', in : { $cond : { if : { $lt : ['$$kvp.v', 50] }, then : 1, else : 0 } } } } }, 2] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_First_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.First(kvp => kvp.Key.StartsWith("l")).Value > 40); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $gt : [{ $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : [{ $indexOfCP : ['$$kvp.k', 'l'] }, 0] } } }, 0] } }, in : '$$this.v' } }, 40] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Select(x => x.Name).Should().BeEquivalentTo("A", "B"); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_IndexerAccess_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface["life"] == 42); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : ['$$kvp.k', 'life'] } } }, 0] } }, in : '$$this.v' } }, 42] } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + result[0].Name.Should().Be("A"); + result[0].DictionaryInterface["life"].Should().Be(42); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_KeysContains_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $elemMatch : { k : 'life' } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_OrderBy_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.ContainsKey("age")) + .OrderBy(x => x.DictionaryInterface["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { DictionaryInterface : { $elemMatch : { k : 'age' } } } }", + "{ $project : { _id : 0, _document : '$$ROOT', _key1 : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : ['$$kvp.k', 'age'] } } }, 0] } }, in : '$$this.v' } } } }", + "{ $sort : { _key1 : 1 } }", + "{ $replaceRoot : { newRoot : '$_document' } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("A"); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_OrderByDescending_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.ContainsKey("age")) + .OrderByDescending(x => x.DictionaryInterface["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { DictionaryInterface : { $elemMatch : { k : 'age' } } } }", + "{ $project : { _id : 0, _document : '$$ROOT', _key1 : { $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : '$DictionaryInterface', as : 'kvp', cond : { $eq : ['$$kvp.k', 'age'] } } }, 0] } }, in : '$$this.v' } } } }", + "{ $sort : { _key1 : -1 } }", + "{ $replaceRoot : { newRoot : '$_document' } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("D"); + } + + [Fact] + public void Where_IDictionaryAsArrayOfDocuments_ValuesContains_should_work() + { + var collection = Fixture.ArrayOfDocsCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { DictionaryInterface : { $elemMatch : { v : 42 } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + } + + [Fact] + public void Where_IDictionaryAsDocument_All_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.All(kvp => kvp.Value > 100)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $allElementsTrue : { $map : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', in : { $gt : ['$$kvp.v', 100] } } } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle().Which.Name.Should().Be("D"); + } + + [Fact] + public void Where_IDictionaryAsDocument_Any_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Any(kvp => kvp.Value > 90)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $anyElementTrue : { $map : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', in : { $gt : ['$$kvp.v', 90] } } } } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(3); + } + + [Fact] + public void Where_IDictionaryAsDocument_ContainsKey_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.ContainsKey("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { 'DictionaryInterface.life' : { $exists : true } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Should().OnlyContain(doc => doc.DictionaryInterface.ContainsKey("life")); + } + + [Fact] + public void Where_IDictionaryAsDocument_Count_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Count == 3); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $size : { $objectToArray : '$DictionaryInterface' } }, 3] } } }"); + + var results = queryable.ToList(); + results.Select(x => x.Name).Should().Equal("A", "B"); + } + + [Fact] + public void Where_IDictionaryAsDocument_CountWithPredicate_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Count(kvp => kvp.Value < 50) == 2); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $eq : [{ $sum : { $map : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', in : { $cond : { if : { $lt : ['$$kvp.v', 50] }, then : 1, else : 0 } } } } }, 2] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_IDictionaryAsDocument_First_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.First(kvp => kvp.Key.StartsWith("l")).Value > 40); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $gt : [{ $let : { vars : { this : { $arrayElemAt : [{ $filter : { input : { $objectToArray : '$DictionaryInterface' }, as : 'kvp', cond : { $eq : [{ $indexOfCP : ['$$kvp.k', 'l'] }, 0] } } }, 0] } }, in : '$$this.v' } }, 40] } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + result.Select(x => x.Name).Should().BeEquivalentTo("A", "B"); + } + + [Fact] + public void Where_IDictionaryAsDocument_IndexerAccess_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface["life"] == 42); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { 'DictionaryInterface.life' : 42 } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + result[0].Name.Should().Be("A"); + result[0].DictionaryInterface["life"].Should().Be(42); + } + + [Fact] + public void Where_IDictionaryAsDocument_KeysContains_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Keys.Contains("life")); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { 'DictionaryInterface.life' : { $exists : true } } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(2); + } + + [Fact] + public void Where_IDictionaryAsDocument_OrderBy_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.ContainsKey("age")) + .OrderBy(x => x.DictionaryInterface["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { 'DictionaryInterface.age' : { $exists : true } } }", + "{ $sort : { 'DictionaryInterface.age' : 1 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("A"); + } + + [Fact] + public void Where_IDictionaryAsDocument_OrderByDescending_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.ContainsKey("age")) + .OrderByDescending(x => x.DictionaryInterface["age"]); + + var stages = Translate(collection, queryable); + AssertStages(stages, + "{ $match : { 'DictionaryInterface.age' : { $exists : true } } }", + "{ $sort : { 'DictionaryInterface.age' : -1 } }"); + + var result = queryable.ToList(); + result.Should().HaveCount(4); + result.First().Name.Should().Be("D"); + } + + [Fact] + public void Where_IDictionaryAsDocument_ValuesContains_should_work() + { + var collection = Fixture.DocCollection; + + var queryable = collection.AsQueryable() + .Where(x => x.DictionaryInterface.Values.Contains(42)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { $expr : { $reduce : { input : { $objectToArray : '$DictionaryInterface' }, initialValue : false, in : { $cond : { if : '$$value', then : true, else : { $eq : ['$$this.v', 42] } } } } } } }"); + + var result = queryable.ToList(); + result.Should().ContainSingle(); + } + + public class DocumentRepresentation + { + public ObjectId Id { get; set; } + public string Name { get; set; } + + [BsonDictionaryOptions(DictionaryRepresentation.Document)] + public Dictionary Dictionary { get; set; } + + [BsonDictionaryOptions(DictionaryRepresentation.Document)] + public IDictionary DictionaryInterface { get; set; } + } + + public class ArrayOfArraysRepresentation + { + public ObjectId Id { get; set; } + public string Name { get; set; } + + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] + public Dictionary Dictionary { get; set; } + + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] + public IDictionary DictionaryInterface { get; set; } + } + + public class ArrayOfDocumentsRepresentation + { + public ObjectId Id { get; set; } + public string Name { get; set; } + + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] + public Dictionary Dictionary { get; set; } + + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] + public IDictionary DictionaryInterface { get; set; } + } + + public sealed class ClassFixture : MongoDatabaseFixture + { + public IMongoCollection DocCollection { get; private set; } + public IMongoCollection ArrayOfArraysCollection { get; private set; } + public IMongoCollection ArrayOfDocsCollection { get; private set; } + + protected override void InitializeFixture() + { + DocCollection = Database.GetCollection("test_document"); + ArrayOfArraysCollection = Database.GetCollection("test_array_of_arrays"); + ArrayOfDocsCollection = Database.GetCollection("test_array_of_docs"); + + SeedTestDictionary(); + } + + private void SeedTestDictionary() + { + // Clear existing Dictionary + DocCollection.DeleteMany(FilterDefinition.Empty); + ArrayOfArraysCollection.DeleteMany(FilterDefinition.Empty); + ArrayOfDocsCollection.DeleteMany(FilterDefinition.Empty); + + // Insert test Dictionary for Document representation + var docDictionary = new List + { + new() + { + Name = "A", + Dictionary = new Dictionary { { "life", 42 }, { "age", 25 }, { "score", 100 } }, + DictionaryInterface = new Dictionary { { "life", 42 }, { "age", 25 }, { "score", 100 } } + }, + new() + { + Name = "B", + Dictionary = new Dictionary { { "life", 41 }, { "age", 30 }, { "score", 85 } }, + DictionaryInterface = new Dictionary { { "life", 41 }, { "age", 30 }, { "score", 85 } } + }, + new() + { + Name = "C", + Dictionary = new Dictionary { { "health", 100 }, { "age", 35 } }, + DictionaryInterface = new Dictionary { { "health", 100 }, { "age", 35 } } + }, + new() + { + Name = "D", + Dictionary = new Dictionary { { "health", 200 }, { "age", 130 } }, + DictionaryInterface = new Dictionary { { "health", 200 }, { "age", 130 } } + } + }; + DocCollection.InsertMany(docDictionary); + + // Insert test Dictionary for ArrayOfArrays representation + var arrayDictionary = new List + { + new() + { + Name = "A", + Dictionary = new Dictionary { { "life", 42 }, { "age", 25 }, { "score", 100 } }, + DictionaryInterface = new Dictionary { { "life", 42 }, { "age", 25 }, { "score", 100 } } + }, + new() + { + Name = "B", + Dictionary = new Dictionary { { "life", 41 }, { "age", 30 }, { "score", 85 } }, + DictionaryInterface = new Dictionary { { "life", 41 }, { "age", 30 }, { "score", 85 } } + }, + new() + { + Name = "C", + Dictionary = new Dictionary { { "health", 100 }, { "age", 35 } }, + DictionaryInterface = new Dictionary { { "health", 100 }, { "age", 35 } } + }, + new() + { + Name = "D", + Dictionary = new Dictionary { { "health", 200 }, { "age", 130 } }, + DictionaryInterface = new Dictionary { { "health", 200 }, { "age", 130 } } + } + }; + ArrayOfArraysCollection.InsertMany(arrayDictionary); + + // Insert test Dictionary for ArrayOfDocuments representation + var arrayDocDictionary = new List + { + new() + { + Name = "A", + Dictionary = new Dictionary { { "life", 42 }, { "age", 25 }, { "score", 100 } }, + DictionaryInterface = new Dictionary { { "life", 42 }, { "age", 25 }, { "score", 100 } } + }, + new() + { + Name = "B", + Dictionary = new Dictionary { { "life", 41 }, { "age", 30 }, { "score", 85 } }, + DictionaryInterface = new Dictionary { { "life", 41 }, { "age", 30 }, { "score", 85 } } + }, + new() + { + Name = "C", + Dictionary = new Dictionary { { "health", 100 }, { "age", 35 } }, + DictionaryInterface = new Dictionary { { "health", 100 }, { "age", 35 } } + }, + new() + { + Name = "D", + Dictionary = new Dictionary { { "health", 200 }, { "age", 130 } }, + DictionaryInterface = new Dictionary { { "health", 200 }, { "age", 130 } } + } + }; + ArrayOfDocsCollection.InsertMany(arrayDocDictionary); + } + } +} \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4557Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4557Tests.cs deleted file mode 100644 index 1c761e8a73e..00000000000 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4557Tests.cs +++ /dev/null @@ -1,78 +0,0 @@ -/* 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 FluentAssertions; -using MongoDB.Driver.TestHelpers; -using Xunit; - -namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira -{ - public class CSharp4557Tests : LinqIntegrationTest - { - public CSharp4557Tests(ClassFixture fixture) - : base(fixture) - { - } - - [Fact] - public void Where_with_ContainsKey_should_work() - { - var collection = Fixture.Collection; - - var queryable = collection - .AsQueryable() - .Where(x => x.Foo.ContainsKey("bar")); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $match : { 'Foo.bar' : { $exists : true } } }"); - - var results = queryable.ToList(); - results.Select(x => x.Id).Should().Equal(2); - } - - [Fact] - public void Select_with_ContainsKey_should_work() - { - var collection = Fixture.Collection; - - var queryable = collection - .AsQueryable() - .Select(x => x.Foo.ContainsKey("bar")); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $project : { _v : { $ne : [{ $type : '$Foo.bar' }, 'missing'] }, _id : 0 } }"); - - var results = queryable.ToList(); - results.Should().Equal(false, true); - } - - public class C - { - public int Id { get; set; } - public Dictionary Foo { get; set; } - } - - public sealed class ClassFixture : MongoCollectionFixture - { - protected override IEnumerable InitialData => - [ - new C { Id = 1, Foo = new Dictionary { { "foo", 100 } } }, - new C { Id = 2, Foo = new Dictionary { { "bar", 100 } } } - ]; - } - } -} diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4813Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4813Tests.cs index 645237a3a33..cd72c16f41a 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4813Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4813Tests.cs @@ -17,8 +17,6 @@ using System.Collections.Generic; using System.Linq; using FluentAssertions; -using MongoDB.Bson.Serialization.Attributes; -using MongoDB.Bson.Serialization.Options; using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; @@ -62,90 +60,6 @@ public void Where_Count_should_work() results.Select(x => x.Id).Should().Equal(1); } - [Fact] - public void Where_Dictionary_Count_should_throw() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Where(x => x.Dictionary.Count == 1); - - var exception = Record.Exception(() => Translate(collection, queryable)); - exception.Should().BeOfType(); - } - - [Fact] - public void Where_DictionaryAsArrayOfArrays_Count_should_work() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Where(x => x.DictionaryAsArrayOfArrays.Count == 1); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $match : { DictionaryAsArrayOfArrays : { $size : 1 } } }"); - - var results = queryable.ToList(); - results.Select(x => x.Id).Should().Equal(1); - } - - [Fact] - public void Where_DictionaryAsArrayOfDocuments_Count_should_work() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Where(x => x.DictionaryAsArrayOfDocuments.Count == 1); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $match : { DictionaryAsArrayOfDocuments : { $size : 1 } } }"); - - var results = queryable.ToList(); - results.Select(x => x.Id).Should().Equal(1); - } - - [Fact] - public void Where_DictionaryInterface_Count_should_throw() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Where(x => x.DictionaryInterface.Count == 1); - - var exception = Record.Exception(() => Translate(collection, queryable)); - exception.Should().BeOfType(); - } - - [Fact] - public void Where_DictionaryInterfaceArrayOfArrays_Count_should_work() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Where(x => x.DictionaryInterfaceAsArrayOfArrays.Count == 1); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $match : { DictionaryInterfaceAsArrayOfArrays : { $size : 1 } } }"); - - var results = queryable.ToList(); - results.Select(x => x.Id).Should().Equal(1); - } - - [Fact] - public void Where_DictionaryInterfaceArrayOfDocuments_Count_should_work() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Where(x => x.DictionaryInterfaceAsArrayOfDocuments.Count == 1); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $match : { DictionaryInterfaceAsArrayOfDocuments : { $size : 1 } } }"); - - var results = queryable.ToList(); - results.Select(x => x.Id).Should().Equal(1); - } - [Fact] public void Where_List_Count_should_work() { @@ -220,124 +134,6 @@ public void Select_Count_should_work() results.Should().Equal(1, 2); } - [Theory] - [ParameterAttributeData] - public void Select_Dictionary_Count_should_throw( - [Values(false, true)] bool enableClientSideProjections) - { - RequireServer.Check().Supports(Feature.FindProjectionExpressions); - var collection = Fixture.Collection; - var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; - - var queryable = collection.AsQueryable(translationOptions) - .Select(x => x.Dictionary.Count); - - if (enableClientSideProjections) - { - var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, "{ $project : { _snippets : ['$Dictionary'], _id : 0 } }"); - outputSerializer.Should().BeAssignableTo(); - - var results = queryable.ToList(); - results.Should().Equal(1, 2); - } - else - { - var exception = Record.Exception(() => Translate(collection, queryable)); - exception.Should().BeOfType(); - exception.Message.Should().Contain("is not represented as an array"); - } - } - - [Fact] - public void Select_DictionaryAsArrayOfArrays_Count_should_work() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Select(x => x.DictionaryAsArrayOfArrays.Count); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $project : { _v : { $size : '$DictionaryAsArrayOfArrays' }, _id : 0 } }"); - - var results = queryable.ToList(); - results.Should().Equal(1, 2); - } - - [Fact] - public void Select_DictionaryAsArrayOfDocuments_Count_should_work() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Select(x => x.DictionaryAsArrayOfDocuments.Count); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $project : { _v : { $size : '$DictionaryAsArrayOfDocuments' }, _id : 0 } }"); - - var results = queryable.ToList(); - results.Should().Equal(1, 2); - } - - [Theory] - [ParameterAttributeData] - public void Select_DictionaryInterface_Count_should_throw( - [Values(false, true)] bool enableClientSideProjections) - { - RequireServer.Check().Supports(Feature.FindProjectionExpressions); - var collection = Fixture.Collection; - var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; - - var queryable = collection.AsQueryable(translationOptions) - .Select(x => x.DictionaryInterface.Count); - - if (enableClientSideProjections) - { - var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, "{ $project : { _snippets : ['$DictionaryInterface'], _id : 0 } }"); - outputSerializer.Should().BeAssignableTo(); - - var results = queryable.ToList(); - results.Should().Equal(1, 2); - } - else - { - var exception = Record.Exception(() => Translate(collection, queryable)); - exception.Should().BeOfType(); - exception.Message.Should().Contain("is not represented as an array"); - } - } - - [Fact] - public void Select_DictionaryInterfaceAsArrayOfArrays_Count_should_work() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Select(x => x.DictionaryInterfaceAsArrayOfArrays.Count); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $project : { _v : { $size : '$DictionaryInterfaceAsArrayOfArrays' }, _id : 0 } }"); - - var results = queryable.ToList(); - results.Should().Equal(1, 2); - } - - [Fact] - public void Select_DictionaryInterfaceAsArrayOfDocuments_Count_should_work() - { - var collection = Fixture.Collection; - - var queryable = collection.AsQueryable() - .Select(x => x.DictionaryInterfaceAsArrayOfDocuments.Count); - - var stages = Translate(collection, queryable); - AssertStages(stages, "{ $project : { _v : { $size : '$DictionaryInterfaceAsArrayOfDocuments' }, _id : 0 } }"); - - var results = queryable.ToList(); - results.Should().Equal(1, 2); - } - [Fact] public void Select_List_Count_should_work() { @@ -373,12 +169,6 @@ public class C public int Id { get; set; } public BitArray BitArray { get; set; } public int Count { get; set; } - public Dictionary Dictionary { get; set; } - [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] public Dictionary DictionaryAsArrayOfArrays { get; set; } - [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] public Dictionary DictionaryAsArrayOfDocuments { get; set; } - public IDictionary DictionaryInterface { get; set; } - [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] public IDictionary DictionaryInterfaceAsArrayOfArrays { get; set; } - [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)] public IDictionary DictionaryInterfaceAsArrayOfDocuments { get; set; } public List List { get; set; } public IList ListInterface { get; set; } } @@ -392,12 +182,6 @@ public sealed class ClassFixture : MongoCollectionFixture Id = 1, BitArray = new BitArray(length: 1), Count = 1, - Dictionary = new() { { "A", 1 } }, - DictionaryAsArrayOfArrays = new() { { "A", 1 } }, - DictionaryAsArrayOfDocuments = new() { { "A", 1 } }, - DictionaryInterface = new Dictionary { { "A", 1 } }, - DictionaryInterfaceAsArrayOfArrays = new Dictionary { { "A", 1 } }, - DictionaryInterfaceAsArrayOfDocuments = new Dictionary { { "A", 1 } }, List = new() { 1 }, ListInterface = new List() { 1 } }, @@ -406,12 +190,6 @@ public sealed class ClassFixture : MongoCollectionFixture Id = 2, BitArray = new BitArray(length: 2), Count = 2, - Dictionary = new() { { "A", 1 }, { "B", 2 } }, - DictionaryAsArrayOfArrays = new() { { "A", 1 }, { "B", 2 } }, - DictionaryAsArrayOfDocuments = new() { { "A", 1 }, { "B", 2 } }, - DictionaryInterface = new Dictionary { { "A", 1 }, { "B", 2 } }, - DictionaryInterfaceAsArrayOfArrays = new Dictionary { { "A", 1 }, { "B", 2 } }, - DictionaryInterfaceAsArrayOfDocuments = new Dictionary { { "A", 1 }, { "B", 2 } }, List = new() { 1, 2 }, ListInterface = new List { 1, 2 } }