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..6a86c058139 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsMethodToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsMethodToAggregationExpressionTranslator.cs @@ -79,8 +79,14 @@ private static TranslatedExpression TranslateEnumerableContains(TranslationConte NestedAsQueryableHelper.EnsureQueryableMethodHasNestedAsQueryableSource(expression, sourceTranslation); var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression); - var ast = AstExpression.In(valueTranslation.Ast, sourceTranslation.Ast); + var itemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer); + if (!itemSerializer.Equals(valueTranslation.Serializer)) + { + throw new ExpressionNotSupportedException(expression, because: "the array items and the value are serialized differently"); + } + + var ast = AstExpression.In(valueTranslation.Ast, sourceTranslation.Ast); return new TranslatedExpression(expression, ast, BooleanSerializer.Instance); } } 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..b8066230a7c 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/AllOrAnyMethodToFilterTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/AllOrAnyMethodToFilterTranslator.cs @@ -42,6 +42,11 @@ public static AstFilter Translate(TranslationContext context, MethodCallExpressi return AnyWithContainsInPredicateMethodToFilterTranslator.Translate(context, arrayFieldExpression, arrayConstantExpression); } + if (AnyWithArrayConstantAndItemEqualsFieldPredicateMethodToFilterTranslator.CanTranslate(expression, out arrayConstantExpression, out var fieldExpression)) + { + return AnyWithArrayConstantAndItemEqualsFieldPredicateMethodToFilterTranslator.Translate(context, arrayConstantExpression, fieldExpression); + } + var method = expression.Method; var arguments = expression.Arguments; diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/AnyWithArrayConstantAndItemEqualsFieldPredicateMethodToFilterTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/AnyWithArrayConstantAndItemEqualsFieldPredicateMethodToFilterTranslator.cs new file mode 100644 index 00000000000..ab2d9f05fd6 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/AnyWithArrayConstantAndItemEqualsFieldPredicateMethodToFilterTranslator.cs @@ -0,0 +1,104 @@ +/* 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; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Reflection; +using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ToFilterFieldTranslators; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.MethodTranslators +{ + internal static class AnyWithArrayConstantAndItemEqualsFieldPredicateMethodToFilterTranslator + { + private static readonly MethodInfo[] __anyWithPredicateMethods = + [ + EnumerableMethod.AnyWithPredicate, + QueryableMethod.AnyWithPredicate, + ]; + + public static bool CanTranslate(MethodCallExpression expression, out ConstantExpression arrayConstantExpression, out Expression fieldExpression) + { + var method = expression.Method; + var arguments = expression.Arguments; + + // arrayConstant.Any(item => item == ) + // arrayConstant.Any(item => == item) + if (method.IsOneOf(__anyWithPredicateMethods)) + { + var sourceExpression = arguments[0]; + var predicateExpression = ExpressionHelper.UnquoteLambdaIfQueryableMethod(method, arguments[1]); + + if (sourceExpression.Type.IsArray && + sourceExpression is ConstantExpression constantExpression) + { + arrayConstantExpression = constantExpression; + + var parameter = predicateExpression.Parameters.Single(); + var body = predicateExpression.Body; + + if (IsItemEqualsFieldComparison(body, parameter, out fieldExpression)) + { + return true; + } + } + } + + arrayConstantExpression = null; + fieldExpression = null; + return false; + } + + public static AstFilter Translate(TranslationContext context, ConstantExpression arrayConstantExpression, Expression fieldExpression) + { + var fieldTranslation = ExpressionToFilterFieldTranslator.Translate(context, fieldExpression); + var itemSerializer = fieldTranslation.Serializer; + var values = (IEnumerable)arrayConstantExpression.Value; + var serializedArrayValues = SerializationHelper.SerializeValues(itemSerializer, values); + return AstFilter.In(fieldTranslation.Ast, serializedArrayValues); + } + + private static bool IsItemEqualsFieldComparison( + Expression expression, + ParameterExpression parameter, + out Expression fieldExpression) + { + if (expression is BinaryExpression binaryExpression && + binaryExpression.NodeType == ExpressionType.Equal) + { + var left = binaryExpression.Left; + var right = binaryExpression.Right; + + if (left == parameter) + { + fieldExpression = right; // defer to Translate to throw if fieldExpression can't be translated to a field + return true; + } + + if (right == parameter) + { + fieldExpression = left; // defer to Translate to throw if fieldExpression can't be translated to a field + return true; + } + } + + fieldExpression = null; + return false; + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5519Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5519Tests.cs new file mode 100644 index 00000000000..7a1f0712505 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5519Tests.cs @@ -0,0 +1,189 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System.Collections.Generic; +using System.Linq; +using MongoDB.Driver.TestHelpers; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.TestHelpers.XunitExtensions; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira; + +public class CSharp5519Tests : LinqIntegrationTest +{ + public CSharp5519Tests(ClassFixture fixture) + : base(fixture) + { + } + + [Theory] + [ParameterAttributeData] + public void Filter_Array_Any_item_equals_field_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + var array = new string[] { "0102030405060708090a0b0c" }; + var find = withNestedAsQueryable ? + collection.Find(t => array.AsQueryable().Any(item => item == t.Id)) : + collection.Find(t => array.Any(item => item == t.Id)); + + var filter = TranslateFindFilter(collection, find); + + filter.Should().Be("{ _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } }"); + } + + [Theory] + [ParameterAttributeData] + public void Filter_Array_Any_field_equals_item_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + var array = new string[] { "0102030405060708090a0b0c" }; + var find = withNestedAsQueryable ? + collection.Find(t => array.AsQueryable().Any(item => t.Id == item)) : + collection.Find(t => array.Any(item => t.Id == item)); + + var filter = TranslateFindFilter(collection, find); + + filter.Should().Be("{ _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } }"); + } + + [Theory] + [ParameterAttributeData] + public void Filter_Array_Contains_field_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + var array = new string[] { "0102030405060708090a0b0c" }; + var find = withNestedAsQueryable ? + collection.Find(t => array.AsQueryable().Contains(t.Id)) : + collection.Find(t => array.Contains(t.Id)); + + var filter = TranslateFindFilter(collection, find); + + filter.Should().Be("{ _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } }"); + } + + [Theory] + [ParameterAttributeData] + public void Where_Array_Any_item_equals_field_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + var array = new string[] { "0102030405060708090a0b0c" }; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Where(t => array.AsQueryable().Any(item => item == t.Id)) : + collection.AsQueryable().Where(t => array.Any(item => item == t.Id)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } } }"); + } + + [Theory] + [ParameterAttributeData] + public void Where_Array_Any_field_equals_item_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + var array = new string[] { "0102030405060708090a0b0c" }; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Where(t => array.AsQueryable().Any(item => t.Id == item)) : + collection.AsQueryable().Where(t => array.Any(item => t.Id == item)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } } }"); + } + + [Theory] + [ParameterAttributeData] + public void Where_Array_Contains_field_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + var array = new string[] { "0102030405060708090a0b0c" }; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Where(t => array.AsQueryable().Contains(t.Id)) : + collection.AsQueryable().Where(t => array.Contains(t.Id)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $match : { _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } } }"); + } + + [Theory] + [ParameterAttributeData] + public void Select_Array_Any_item_equals_field_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + var array = new string[] { "0102030405060708090a0b0c" }; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(t => array.AsQueryable().Any(item => item == t.Id)) : + collection.AsQueryable().Select(t => array.Any(item => item == t.Id)); + + var exception = Record.Exception(() => Translate(collection, queryable)); // TODO: support? + exception.Message.Should().Contain("Expression not supported: (id == t.Id) because the two arguments are serialized differently"); + } + + [Theory] + [ParameterAttributeData] + public void Select_Array_Any_field_equals_item_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + var array = new string[] { "0102030405060708090a0b0c" }; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(t => array.AsQueryable().Any(item => t.Id == item)) : + collection.AsQueryable().Select(t => array.Any(item => t.Id == item)); + + var exception = Record.Exception(() => Translate(collection, queryable)); // TODO: support? + exception.Message.Should().Contain("Expression not supported: (id == t.Id) because the two arguments are serialized differently"); + } + + [Theory] + [ParameterAttributeData] + public void Select_Array_Contains_field_should_work( + [Values(false, true)] bool withNestedAsQueryable) + { + var collection = Fixture.Collection; + var array = new string[] { "0102030405060708090a0b0c" }; + + var queryable = withNestedAsQueryable ? + collection.AsQueryable().Select(t => array.AsQueryable().Contains(t.Id)) : + collection.AsQueryable().Select(t => array.Contains(t.Id)); + + var exception = Record.Exception(() => Translate(collection, queryable)); // TODO: support? + exception.Message.Should().Contain("Expression not supported: value(System.String[]).Contains(t.Id) because the array items and the value are serialized differently"); + } + + public class Test + { + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData => null; + } +}