From 8ab67ac9307dd440f56e3669572158bfda2ec66c Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:15:21 +0100 Subject: [PATCH 1/7] Improvements --- .../LastMethodToExecutableQueryTranslator.cs | 3 +- ...tMethodToExecutableQueryTranslatorTests.cs | 174 ++++++++++++++++++ 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslator.cs index 4e76f4854c7..70964e72ecf 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslator.cs @@ -75,10 +75,11 @@ public static ExecutableQuery<TDocument, TOutput> Translate<TDocument>(MongoQuer pipeline.OutputSerializer); } - pipeline = pipeline.AddStage( + pipeline = pipeline.AddStages( AstStage.Group( id: BsonNull.Value, fields: AstExpression.AccumulatorField("_last", AstUnaryAccumulatorOperator.Last, AstExpression.RootVar)), + AstStage.ReplaceRoot(AstExpression.GetField(AstExpression.RootVar, "_last")), pipeline.OutputSerializer); var finalizer = method.Name == "LastOrDefault" ? __singleOrDefaultFinalizer : __singleFinalizer; diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs new file mode 100644 index 00000000000..4a205aee688 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs @@ -0,0 +1,174 @@ +/* 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 System.Linq.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation; +using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators; +using MongoDB.Driver.TestHelpers; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators +{ + public class LastMethodToExecutableQueryTranslator : LinqIntegrationTest<LastMethodToExecutableQueryTranslator.ClassFixture> + { + public LastMethodToExecutableQueryTranslator(ClassFixture fixture) + : base(fixture) + { + } + + [Fact] + public void Last_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable().OrderBy(t => t.Id); + var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + + var lastMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 1) + .MakeGenericMethod(typeof(TestClass)); + + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( + provider, + Expression.Call(lastMethod, queryable.Expression), + translationOptions: null); + + var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + + var expectedStages = new[] { + """{ "$sort" : { "_id" : 1 } }""", + """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", + """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + }; + + AssertStages(stages, expectedStages); + var result = queryable.Last(); + Assert.Equal(3, result.Id); + } + + [Fact] + public void Last_with_null_return_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable().Where(t => t.Id > 5).OrderBy(t => t.Id); + var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + + var lastMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 1) + .MakeGenericMethod(typeof(TestClass)); + + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( + provider, + Expression.Call(lastMethod, queryable.Expression), + translationOptions: null); + + var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + + var expectedStages = new[] { + """{ "$match" : { "_id" : { "$gt" : 5 } }}""", + """{ "$sort" : { "_id" : 1 } }""", + """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", + """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + }; + + AssertStages(stages, expectedStages); + var exception = Record.Exception(() => queryable.Last()); + Assert.NotNull(exception); + Assert.IsType<InvalidOperationException>(exception); + } + + [Fact] + public void LastOrDefault_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable().OrderBy(t => t.Id); + var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + + var lastMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.LastOrDefault) && m.GetParameters().Length == 1) + .MakeGenericMethod(typeof(TestClass)); + + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( + provider, + Expression.Call(lastMethod, queryable.Expression), + translationOptions: null); + + var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + + var expectedStages = new[] { + """{ "$sort" : { "_id" : 1 } }""", + """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", + """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + }; + + AssertStages(stages, expectedStages); + var result = queryable.LastOrDefault(); + Assert.NotNull(result); + Assert.Equal(3, result.Id); + } + + [Fact] + public void LastOrDefault_with_null_return_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable().Where(t => t.Id > 5).OrderBy(t => t.Id); + var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + + var lastMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.LastOrDefault) && m.GetParameters().Length == 1) + .MakeGenericMethod(typeof(TestClass)); + + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( + provider, + Expression.Call(lastMethod, queryable.Expression), + translationOptions: null); + + var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + + var expectedStages = new[] { + """{ "$match" : { "_id" : { "$gt" : 5 } }}""", + """{ "$sort" : { "_id" : 1 } }""", + """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", + """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + }; + + AssertStages(stages, expectedStages); + var result = queryable.LastOrDefault(); + Assert.Null(result); + } + + public class TestClass + { + public int Id { get; set; } + public string StringProperty { get; set; } + } + + public sealed class ClassFixture : MongoCollectionFixture<TestClass> + { + protected override IEnumerable<TestClass> InitialData { get; } = + [ + new TestClass { Id = 1, StringProperty = "AB" }, + new TestClass { Id = 2, StringProperty = "ABC" }, + new TestClass { Id = 3, StringProperty = "ABCDE" } + ]; + } + } +} \ No newline at end of file From 0e0829e3f2f266c88f10bce6c1e8af7377d1e5aa Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:26:17 +0100 Subject: [PATCH 2/7] Finished tests --- ...tMethodToExecutableQueryTranslatorTests.cs | 136 +++++++++++++++++- 1 file changed, 135 insertions(+), 1 deletion(-) diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs index 4a205aee688..be35d221c32 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs @@ -31,6 +31,140 @@ public LastMethodToExecutableQueryTranslator(ClassFixture fixture) { } + [Fact] + public void LastWithPredicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable().OrderBy(t => t.Id); + var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + + var lastMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) + .MakeGenericMethod(typeof(TestClass)); + + Expression<Func<TestClass, bool>> exp = t => t.Id > 1; + + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( + provider, + Expression.Call(lastMethod, queryable.Expression, exp), + translationOptions: null); + + var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + + var expectedStages = new[] { + """{ "$sort" : { "_id" : 1 } }""", + """{ "$match" : { "_id" : { "$gt" : 1 } }}""", + """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", + """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + }; + + AssertStages(stages, expectedStages); + var result = queryable.Last(exp); + Assert.Equal(3, result.Id); + } + + [Fact] + public void LastWithPredicate_and_null_return_should_throw() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable().OrderBy(t => t.Id); + var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + + var lastMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) + .MakeGenericMethod(typeof(TestClass)); + + Expression<Func<TestClass, bool>> exp = t => t.Id > 4; + + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( + provider, + Expression.Call(lastMethod, queryable.Expression, exp), + translationOptions: null); + + var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + + var expectedStages = new[] { + """{ "$sort" : { "_id" : 1 } }""", + """{ "$match" : { "_id" : { "$gt" : 4 } }}""", + """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", + """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + }; + + AssertStages(stages, expectedStages); + var exception = Record.Exception(() => queryable.Last(exp)); + Assert.NotNull(exception); + Assert.IsType<InvalidOperationException>(exception); + } + + [Fact] + public void LastOrDefaultWithPredicate_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable().OrderBy(t => t.Id); + var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + + var lastMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) + .MakeGenericMethod(typeof(TestClass)); + + Expression<Func<TestClass, bool>> exp = t => t.Id > 1; + + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( + provider, + Expression.Call(lastMethod, queryable.Expression, exp), + translationOptions: null); + + var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + + var expectedStages = new[] { + """{ "$sort" : { "_id" : 1 } }""", + """{ "$match" : { "_id" : { "$gt" : 1 } }}""", + """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", + """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + }; + + AssertStages(stages, expectedStages); + var result = queryable.LastOrDefault(exp); + Assert.NotNull(result); + Assert.Equal(3, result.Id); + } + + [Fact] + public void LastOrDefaultWithPredicate_and_null_return_should_work() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable().OrderBy(t => t.Id); + var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + + var lastMethod = typeof(Queryable).GetMethods() + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) + .MakeGenericMethod(typeof(TestClass)); + + Expression<Func<TestClass, bool>> exp = t => t.Id > 4; + + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( + provider, + Expression.Call(lastMethod, queryable.Expression, exp), + translationOptions: null); + + var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + + var expectedStages = new[] { + """{ "$sort" : { "_id" : 1 } }""", + """{ "$match" : { "_id" : { "$gt" : 4 } }}""", + """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", + """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + }; + + AssertStages(stages, expectedStages); + var result = queryable.LastOrDefault(exp); + Assert.Null(result); + } + [Fact] public void Last_should_work() { @@ -62,7 +196,7 @@ public void Last_should_work() } [Fact] - public void Last_with_null_return_should_work() + public void Last_with_null_return_should_throw() { var collection = Fixture.Collection; From f242c695cb59046b0ee2e2a1e1697daf5cb376c1 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:29:43 +0100 Subject: [PATCH 3/7] Reorder alphabetically --- ...tMethodToExecutableQueryTranslatorTests.cs | 110 +++++++++--------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs index be35d221c32..28776ecf124 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs @@ -32,7 +32,7 @@ public LastMethodToExecutableQueryTranslator(ClassFixture fixture) } [Fact] - public void LastWithPredicate_should_work() + public void Last_should_work() { var collection = Fixture.Collection; @@ -40,66 +40,61 @@ public void LastWithPredicate_should_work() var provider = (MongoQueryProvider<TestClass>)queryable.Provider; var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 1) .MakeGenericMethod(typeof(TestClass)); - Expression<Func<TestClass, bool>> exp = t => t.Id > 1; - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( provider, - Expression.Call(lastMethod, queryable.Expression, exp), + Expression.Call(lastMethod, queryable.Expression), translationOptions: null); var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); var expectedStages = new[] { """{ "$sort" : { "_id" : 1 } }""", - """{ "$match" : { "_id" : { "$gt" : 1 } }}""", """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.Last(exp); + var result = queryable.Last(); Assert.Equal(3, result.Id); } [Fact] - public void LastWithPredicate_and_null_return_should_throw() + public void Last_with_null_return_should_throw() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().OrderBy(t => t.Id); + var queryable = collection.AsQueryable().Where(t => t.Id > 5).OrderBy(t => t.Id); var provider = (MongoQueryProvider<TestClass>)queryable.Provider; var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 1) .MakeGenericMethod(typeof(TestClass)); - Expression<Func<TestClass, bool>> exp = t => t.Id > 4; - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( provider, - Expression.Call(lastMethod, queryable.Expression, exp), + Expression.Call(lastMethod, queryable.Expression), translationOptions: null); var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); var expectedStages = new[] { + """{ "$match" : { "_id" : { "$gt" : 5 } }}""", """{ "$sort" : { "_id" : 1 } }""", - """{ "$match" : { "_id" : { "$gt" : 4 } }}""", """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" }; AssertStages(stages, expectedStages); - var exception = Record.Exception(() => queryable.Last(exp)); + var exception = Record.Exception(() => queryable.Last()); Assert.NotNull(exception); Assert.IsType<InvalidOperationException>(exception); } [Fact] - public void LastOrDefaultWithPredicate_should_work() + public void LastOrDefault_should_work() { var collection = Fixture.Collection; @@ -107,66 +102,61 @@ public void LastOrDefaultWithPredicate_should_work() var provider = (MongoQueryProvider<TestClass>)queryable.Provider; var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) + .First(m => m.Name == nameof(Queryable.LastOrDefault) && m.GetParameters().Length == 1) .MakeGenericMethod(typeof(TestClass)); - Expression<Func<TestClass, bool>> exp = t => t.Id > 1; - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( provider, - Expression.Call(lastMethod, queryable.Expression, exp), + Expression.Call(lastMethod, queryable.Expression), translationOptions: null); var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); var expectedStages = new[] { """{ "$sort" : { "_id" : 1 } }""", - """{ "$match" : { "_id" : { "$gt" : 1 } }}""", """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.LastOrDefault(exp); + var result = queryable.LastOrDefault(); Assert.NotNull(result); Assert.Equal(3, result.Id); } [Fact] - public void LastOrDefaultWithPredicate_and_null_return_should_work() + public void LastOrDefault_with_null_return_should_work() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().OrderBy(t => t.Id); + var queryable = collection.AsQueryable().Where(t => t.Id > 5).OrderBy(t => t.Id); var provider = (MongoQueryProvider<TestClass>)queryable.Provider; var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) + .First(m => m.Name == nameof(Queryable.LastOrDefault) && m.GetParameters().Length == 1) .MakeGenericMethod(typeof(TestClass)); - Expression<Func<TestClass, bool>> exp = t => t.Id > 4; - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( provider, - Expression.Call(lastMethod, queryable.Expression, exp), + Expression.Call(lastMethod, queryable.Expression), translationOptions: null); var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); var expectedStages = new[] { + """{ "$match" : { "_id" : { "$gt" : 5 } }}""", """{ "$sort" : { "_id" : 1 } }""", - """{ "$match" : { "_id" : { "$gt" : 4 } }}""", """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.LastOrDefault(exp); + var result = queryable.LastOrDefault(); Assert.Null(result); } [Fact] - public void Last_should_work() + public void LastOrDefaultWithPredicate_should_work() { var collection = Fixture.Collection; @@ -174,61 +164,66 @@ public void Last_should_work() var provider = (MongoQueryProvider<TestClass>)queryable.Provider; var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 1) + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) .MakeGenericMethod(typeof(TestClass)); + Expression<Func<TestClass, bool>> exp = t => t.Id > 1; + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( provider, - Expression.Call(lastMethod, queryable.Expression), + Expression.Call(lastMethod, queryable.Expression, exp), translationOptions: null); var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); var expectedStages = new[] { """{ "$sort" : { "_id" : 1 } }""", + """{ "$match" : { "_id" : { "$gt" : 1 } }}""", """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.Last(); + var result = queryable.LastOrDefault(exp); + Assert.NotNull(result); Assert.Equal(3, result.Id); } [Fact] - public void Last_with_null_return_should_throw() + public void LastOrDefaultWithPredicate_and_null_return_should_work() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().Where(t => t.Id > 5).OrderBy(t => t.Id); + var queryable = collection.AsQueryable().OrderBy(t => t.Id); var provider = (MongoQueryProvider<TestClass>)queryable.Provider; var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 1) + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) .MakeGenericMethod(typeof(TestClass)); + Expression<Func<TestClass, bool>> exp = t => t.Id > 4; + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( provider, - Expression.Call(lastMethod, queryable.Expression), + Expression.Call(lastMethod, queryable.Expression, exp), translationOptions: null); var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); var expectedStages = new[] { - """{ "$match" : { "_id" : { "$gt" : 5 } }}""", """{ "$sort" : { "_id" : 1 } }""", + """{ "$match" : { "_id" : { "$gt" : 4 } }}""", """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" }; AssertStages(stages, expectedStages); - var exception = Record.Exception(() => queryable.Last()); - Assert.NotNull(exception); - Assert.IsType<InvalidOperationException>(exception); + var result = queryable.LastOrDefault(exp); + Assert.Null(result); } [Fact] - public void LastOrDefault_should_work() + public void LastWithPredicate_should_work() { var collection = Fixture.Collection; @@ -236,57 +231,62 @@ public void LastOrDefault_should_work() var provider = (MongoQueryProvider<TestClass>)queryable.Provider; var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.LastOrDefault) && m.GetParameters().Length == 1) + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) .MakeGenericMethod(typeof(TestClass)); + Expression<Func<TestClass, bool>> exp = t => t.Id > 1; + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( provider, - Expression.Call(lastMethod, queryable.Expression), + Expression.Call(lastMethod, queryable.Expression, exp), translationOptions: null); var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); var expectedStages = new[] { """{ "$sort" : { "_id" : 1 } }""", + """{ "$match" : { "_id" : { "$gt" : 1 } }}""", """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.LastOrDefault(); - Assert.NotNull(result); + var result = queryable.Last(exp); Assert.Equal(3, result.Id); } [Fact] - public void LastOrDefault_with_null_return_should_work() + public void LastWithPredicate_and_null_return_should_throw() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().Where(t => t.Id > 5).OrderBy(t => t.Id); + var queryable = collection.AsQueryable().OrderBy(t => t.Id); var provider = (MongoQueryProvider<TestClass>)queryable.Provider; var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.LastOrDefault) && m.GetParameters().Length == 1) + .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) .MakeGenericMethod(typeof(TestClass)); + Expression<Func<TestClass, bool>> exp = t => t.Id > 4; + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( provider, - Expression.Call(lastMethod, queryable.Expression), + Expression.Call(lastMethod, queryable.Expression, exp), translationOptions: null); var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); var expectedStages = new[] { - """{ "$match" : { "_id" : { "$gt" : 5 } }}""", """{ "$sort" : { "_id" : 1 } }""", + """{ "$match" : { "_id" : { "$gt" : 4 } }}""", """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.LastOrDefault(); - Assert.Null(result); + var exception = Record.Exception(() => queryable.Last(exp)); + Assert.NotNull(exception); + Assert.IsType<InvalidOperationException>(exception); } public class TestClass @@ -305,4 +305,4 @@ public sealed class ClassFixture : MongoCollectionFixture<TestClass> ]; } } -} \ No newline at end of file +} From 54fabe58363a998666db7570dfc82ba28ef3a80f Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:06:31 +0100 Subject: [PATCH 4/7] Simplification --- ...tMethodToExecutableQueryTranslatorTests.cs | 134 +++++------------- 1 file changed, 37 insertions(+), 97 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs index 28776ecf124..30c57b13d3c 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs @@ -17,6 +17,8 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; +using MongoDB.Bson; using MongoDB.Driver.Linq.Linq3Implementation; using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators; using MongoDB.Driver.TestHelpers; @@ -35,20 +37,10 @@ public LastMethodToExecutableQueryTranslator(ClassFixture fixture) public void Last_should_work() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var provider = (MongoQueryProvider<TestClass>)queryable.Provider; - - var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 1) - .MakeGenericMethod(typeof(TestClass)); - - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( - provider, - Expression.Call(lastMethod, queryable.Expression), - translationOptions: null); + var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 1); - var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + var stages = GetStages(queryable, lastMethod); var expectedStages = new[] { """{ "$sort" : { "_id" : 1 } }""", @@ -65,20 +57,10 @@ public void Last_should_work() public void Last_with_null_return_should_throw() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().Where(t => t.Id > 5).OrderBy(t => t.Id); - var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 1); - var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 1) - .MakeGenericMethod(typeof(TestClass)); - - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( - provider, - Expression.Call(lastMethod, queryable.Expression), - translationOptions: null); - - var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + var stages = GetStages(queryable, lastMethod); var expectedStages = new[] { """{ "$match" : { "_id" : { "$gt" : 5 } }}""", @@ -97,20 +79,10 @@ public void Last_with_null_return_should_throw() public void LastOrDefault_should_work() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var provider = (MongoQueryProvider<TestClass>)queryable.Provider; - - var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.LastOrDefault) && m.GetParameters().Length == 1) - .MakeGenericMethod(typeof(TestClass)); - - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( - provider, - Expression.Call(lastMethod, queryable.Expression), - translationOptions: null); + var lastMethod = GetQueryableMethod(nameof(Queryable.LastOrDefault), 1); - var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + var stages = GetStages(queryable, lastMethod); var expectedStages = new[] { """{ "$sort" : { "_id" : 1 } }""", @@ -128,20 +100,10 @@ public void LastOrDefault_should_work() public void LastOrDefault_with_null_return_should_work() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().Where(t => t.Id > 5).OrderBy(t => t.Id); - var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + var lastMethod = GetQueryableMethod(nameof(Queryable.LastOrDefault), 1); - var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.LastOrDefault) && m.GetParameters().Length == 1) - .MakeGenericMethod(typeof(TestClass)); - - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( - provider, - Expression.Call(lastMethod, queryable.Expression), - translationOptions: null); - - var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + var stages = GetStages(queryable, lastMethod); var expectedStages = new[] { """{ "$match" : { "_id" : { "$gt" : 5 } }}""", @@ -159,22 +121,12 @@ public void LastOrDefault_with_null_return_should_work() public void LastOrDefaultWithPredicate_should_work() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var provider = (MongoQueryProvider<TestClass>)queryable.Provider; - - var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) - .MakeGenericMethod(typeof(TestClass)); + var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 2); Expression<Func<TestClass, bool>> exp = t => t.Id > 1; - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( - provider, - Expression.Call(lastMethod, queryable.Expression, exp), - translationOptions: null); - - var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + var stages = GetStages(queryable, lastMethod, exp); var expectedStages = new[] { """{ "$sort" : { "_id" : 1 } }""", @@ -193,22 +145,12 @@ public void LastOrDefaultWithPredicate_should_work() public void LastOrDefaultWithPredicate_and_null_return_should_work() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var provider = (MongoQueryProvider<TestClass>)queryable.Provider; - - var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) - .MakeGenericMethod(typeof(TestClass)); + var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 2); Expression<Func<TestClass, bool>> exp = t => t.Id > 4; - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( - provider, - Expression.Call(lastMethod, queryable.Expression, exp), - translationOptions: null); - - var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + var stages = GetStages(queryable, lastMethod, exp); var expectedStages = new[] { """{ "$sort" : { "_id" : 1 } }""", @@ -226,22 +168,12 @@ public void LastOrDefaultWithPredicate_and_null_return_should_work() public void LastWithPredicate_should_work() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var provider = (MongoQueryProvider<TestClass>)queryable.Provider; - - var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) - .MakeGenericMethod(typeof(TestClass)); + var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 2); Expression<Func<TestClass, bool>> exp = t => t.Id > 1; - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( - provider, - Expression.Call(lastMethod, queryable.Expression, exp), - translationOptions: null); - - var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + var stages = GetStages(queryable, lastMethod, exp); var expectedStages = new[] { """{ "$sort" : { "_id" : 1 } }""", @@ -256,25 +188,15 @@ public void LastWithPredicate_should_work() } [Fact] - public void LastWithPredicate_and_null_return_should_throw() + public void LastWithPredicate_with_null_return_should_throw() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var provider = (MongoQueryProvider<TestClass>)queryable.Provider; - - var lastMethod = typeof(Queryable).GetMethods() - .First(m => m.Name == nameof(Queryable.Last) && m.GetParameters().Length == 2) - .MakeGenericMethod(typeof(TestClass)); + var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 2); Expression<Func<TestClass, bool>> exp = t => t.Id > 4; - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( - provider, - Expression.Call(lastMethod, queryable.Expression, exp), - translationOptions: null); - - var stages = executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + var stages = GetStages(queryable, lastMethod, exp); var expectedStages = new[] { """{ "$sort" : { "_id" : 1 } }""", @@ -289,6 +211,24 @@ public void LastWithPredicate_and_null_return_should_throw() Assert.IsType<InvalidOperationException>(exception); } + private static MethodInfo GetQueryableMethod(string methodName, int parameterCount) + { + return typeof(Queryable).GetMethods() + .First(m => m.Name == methodName && m.GetParameters().Length == parameterCount) + .MakeGenericMethod(typeof(TestClass)); + } + + private static List<BsonDocument> GetStages(IQueryable<TestClass> queryable, MethodInfo method, Expression arg = null) + { + var provider = (MongoQueryProvider<TestClass>)queryable.Provider; + var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( + provider, + arg == null ? Expression.Call(method, queryable.Expression): Expression.Call(method, queryable.Expression, arg), + translationOptions: null); + + return executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + } + public class TestClass { public int Id { get; set; } From 863cdf1ca2ae7a3a7859732f2c4d0832d50fa131 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:02:15 +0200 Subject: [PATCH 5/7] Fixes according to PR --- ...tMethodToExecutableQueryTranslatorTests.cs | 167 +++++++----------- 1 file changed, 66 insertions(+), 101 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs index 30c57b13d3c..af5b45f773d 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs @@ -18,7 +18,9 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using FluentAssertions; using MongoDB.Bson; +using MongoDB.Driver.Linq; using MongoDB.Driver.Linq.Linq3Implementation; using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators; using MongoDB.Driver.TestHelpers; @@ -38,41 +40,39 @@ public void Last_should_work() { var collection = Fixture.Collection; var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 1); - var stages = GetStages(queryable, lastMethod); + var result = queryable.Last(); + var stages = queryable.GetMongoQueryProvider().LoggedStages; var expectedStages = new[] { - """{ "$sort" : { "_id" : 1 } }""", - """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", - """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + """{ $sort : { _id : 1 } }""", + """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", + """{ $replaceRoot : { newRoot : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.Last(); - Assert.Equal(3, result.Id); + result.Should().NotBeNull(); + result.Id.Should().Be(3); } [Fact] - public void Last_with_null_return_should_throw() + public void Last_with_no_documents_should_throw() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().Where(t => t.Id > 5).OrderBy(t => t.Id); - var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 1); + var queryable = collection.AsQueryable().Where(t => t.Id > 5); - var stages = GetStages(queryable, lastMethod); + var exception = Record.Exception(() => queryable.Last()); + var stages = queryable.GetMongoQueryProvider().LoggedStages; var expectedStages = new[] { - """{ "$match" : { "_id" : { "$gt" : 5 } }}""", - """{ "$sort" : { "_id" : 1 } }""", - """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", - """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + """{ $match : { _id : { $gt : 5 } }}""", + """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", + """{ $replaceRoot : { newRoot : "$_last" } }""" }; AssertStages(stages, expectedStages); - var exception = Record.Exception(() => queryable.Last()); - Assert.NotNull(exception); - Assert.IsType<InvalidOperationException>(exception); + exception.Should().BeOfType<InvalidOperationException>(); + exception.Message.Should().Contain("Sequence contains no elements"); } [Fact] @@ -80,41 +80,38 @@ public void LastOrDefault_should_work() { var collection = Fixture.Collection; var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var lastMethod = GetQueryableMethod(nameof(Queryable.LastOrDefault), 1); - var stages = GetStages(queryable, lastMethod); + var result = queryable.LastOrDefault(); + var stages = queryable.GetMongoQueryProvider().LoggedStages; var expectedStages = new[] { - """{ "$sort" : { "_id" : 1 } }""", - """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", - """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + """{ $sort : { _id : 1 } }""", + """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", + """{ $replaceRoot : { newRoot : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.LastOrDefault(); - Assert.NotNull(result); - Assert.Equal(3, result.Id); + result.Should().NotBeNull(); + result.Id.Should().Be(3); } [Fact] - public void LastOrDefault_with_null_return_should_work() + public void LastOrDefault_with_no_documents_should_work() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().Where(t => t.Id > 5).OrderBy(t => t.Id); - var lastMethod = GetQueryableMethod(nameof(Queryable.LastOrDefault), 1); + var queryable = collection.AsQueryable().Where(t => t.Id > 5); - var stages = GetStages(queryable, lastMethod); + var result = queryable.LastOrDefault(); + var stages = queryable.GetMongoQueryProvider().LoggedStages; var expectedStages = new[] { - """{ "$match" : { "_id" : { "$gt" : 5 } }}""", - """{ "$sort" : { "_id" : 1 } }""", - """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", - """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + """{ $match : { _id : { $gt : 5 } }}""", + """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", + """{ $replaceRoot : { newRoot : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.LastOrDefault(); - Assert.Null(result); + result.Should().BeNull(); } [Fact] @@ -122,46 +119,39 @@ public void LastOrDefaultWithPredicate_should_work() { var collection = Fixture.Collection; var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 2); - - Expression<Func<TestClass, bool>> exp = t => t.Id > 1; - var stages = GetStages(queryable, lastMethod, exp); + var result = queryable.LastOrDefault(t => t.Id > 1); + var stages = queryable.GetMongoQueryProvider().LoggedStages; var expectedStages = new[] { - """{ "$sort" : { "_id" : 1 } }""", - """{ "$match" : { "_id" : { "$gt" : 1 } }}""", - """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", - """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + """{ $sort : { _id : 1 } }""", + """{ $match : { _id : { $gt : 1 } }}""", + """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", + """{ $replaceRoot : { newRoot : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.LastOrDefault(exp); - Assert.NotNull(result); - Assert.Equal(3, result.Id); + result.Should().NotBeNull(); + result.Id.Should().Be(3); } [Fact] public void LastOrDefaultWithPredicate_and_null_return_should_work() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 2); - - Expression<Func<TestClass, bool>> exp = t => t.Id > 4; + var queryable = collection.AsQueryable(); - var stages = GetStages(queryable, lastMethod, exp); + var result = queryable.LastOrDefault(t => t.Id > 4); + var stages = queryable.GetMongoQueryProvider().LoggedStages; var expectedStages = new[] { - """{ "$sort" : { "_id" : 1 } }""", - """{ "$match" : { "_id" : { "$gt" : 4 } }}""", - """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", - """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + """{ $match : { _id : { $gt : 4 } }}""", + """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", + """{ $replaceRoot : { newRoot : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.LastOrDefault(exp); - Assert.Null(result); + result.Should().BeNull(); } [Fact] @@ -169,64 +159,39 @@ public void LastWithPredicate_should_work() { var collection = Fixture.Collection; var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 2); - - Expression<Func<TestClass, bool>> exp = t => t.Id > 1; - var stages = GetStages(queryable, lastMethod, exp); + var result = queryable.Last(t => t.Id > 1); + var stages = queryable.GetMongoQueryProvider().LoggedStages; var expectedStages = new[] { - """{ "$sort" : { "_id" : 1 } }""", - """{ "$match" : { "_id" : { "$gt" : 1 } }}""", - """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", - """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + """{ $sort : { _id : 1 } }""", + """{ $match : { _id : { $gt : 1 } }}""", + """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", + """{ $replaceRoot : { newRoot : "$_last" } }""" }; AssertStages(stages, expectedStages); - var result = queryable.Last(exp); Assert.Equal(3, result.Id); } [Fact] - public void LastWithPredicate_with_null_return_should_throw() + public void LastWithPredicate_with_no_documents_should_throw() { var collection = Fixture.Collection; - var queryable = collection.AsQueryable().OrderBy(t => t.Id); - var lastMethod = GetQueryableMethod(nameof(Queryable.Last), 2); - - Expression<Func<TestClass, bool>> exp = t => t.Id > 4; + var queryable = collection.AsQueryable(); - var stages = GetStages(queryable, lastMethod, exp); + var exception = Record.Exception(() => queryable.Last(t => t.Id > 4)); + var stages = queryable.GetMongoQueryProvider().LoggedStages; var expectedStages = new[] { - """{ "$sort" : { "_id" : 1 } }""", - """{ "$match" : { "_id" : { "$gt" : 4 } }}""", - """{ "$group" : { "_id" : null, "_last" : { "$last" : "$$ROOT" } } }""", - """{ "$replaceRoot" : { "newRoot" : "$_last" } }""" + """{ $match : { _id : { $gt : 4 } }}""", + """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", + """{ $replaceRoot : { newRoot : "$_last" } }""" }; AssertStages(stages, expectedStages); - var exception = Record.Exception(() => queryable.Last(exp)); - Assert.NotNull(exception); - Assert.IsType<InvalidOperationException>(exception); - } - - private static MethodInfo GetQueryableMethod(string methodName, int parameterCount) - { - return typeof(Queryable).GetMethods() - .First(m => m.Name == methodName && m.GetParameters().Length == parameterCount) - .MakeGenericMethod(typeof(TestClass)); - } - - private static List<BsonDocument> GetStages(IQueryable<TestClass> queryable, MethodInfo method, Expression arg = null) - { - var provider = (MongoQueryProvider<TestClass>)queryable.Provider; - var executableQuery = ExpressionToExecutableQueryTranslator.TranslateScalar<TestClass, TestClass>( - provider, - arg == null ? Expression.Call(method, queryable.Expression): Expression.Call(method, queryable.Expression, arg), - translationOptions: null); - - return executableQuery.Pipeline.Ast.Stages.Select(s => s.Render().AsBsonDocument).ToList(); + exception.Should().BeOfType<InvalidOperationException>(); + exception.Message.Should().Contain("Sequence contains no elements"); } public class TestClass @@ -239,9 +204,9 @@ public sealed class ClassFixture : MongoCollectionFixture<TestClass> { protected override IEnumerable<TestClass> InitialData { get; } = [ - new TestClass { Id = 1, StringProperty = "AB" }, - new TestClass { Id = 2, StringProperty = "ABC" }, - new TestClass { Id = 3, StringProperty = "ABCDE" } + new TestClass { Id = 1, StringProperty = "A" }, + new TestClass { Id = 2, StringProperty = "B" }, + new TestClass { Id = 3, StringProperty = "C" } ]; } } From 0c3dc772454db446de01797f02cb4868ff45a637 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:08:41 +0200 Subject: [PATCH 6/7] Various corrections --- ...tMethodToExecutableQueryTranslatorTests.cs | 72 ++++++++----------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs index af5b45f773d..22871fadccd 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs @@ -16,13 +16,8 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using System.Reflection; using FluentAssertions; -using MongoDB.Bson; using MongoDB.Driver.Linq; -using MongoDB.Driver.Linq.Linq3Implementation; -using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators; using MongoDB.Driver.TestHelpers; using Xunit; @@ -44,19 +39,17 @@ public void Last_should_work() var result = queryable.Last(); var stages = queryable.GetMongoQueryProvider().LoggedStages; - var expectedStages = new[] { + AssertStages( + stages, """{ $sort : { _id : 1 } }""", """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }""" - }; + """{ $replaceRoot : { newRoot : "$_last" } }"""); - AssertStages(stages, expectedStages); - result.Should().NotBeNull(); result.Id.Should().Be(3); } [Fact] - public void Last_with_no_documents_should_throw() + public void Last_with_no_matching_documents_should_throw() { var collection = Fixture.Collection; var queryable = collection.AsQueryable().Where(t => t.Id > 5); @@ -64,13 +57,12 @@ public void Last_with_no_documents_should_throw() var exception = Record.Exception(() => queryable.Last()); var stages = queryable.GetMongoQueryProvider().LoggedStages; - var expectedStages = new[] { + AssertStages( + stages, """{ $match : { _id : { $gt : 5 } }}""", """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }""" - }; + """{ $replaceRoot : { newRoot : "$_last" } }"""); - AssertStages(stages, expectedStages); exception.Should().BeOfType<InvalidOperationException>(); exception.Message.Should().Contain("Sequence contains no elements"); } @@ -84,19 +76,18 @@ public void LastOrDefault_should_work() var result = queryable.LastOrDefault(); var stages = queryable.GetMongoQueryProvider().LoggedStages; - var expectedStages = new[] { + AssertStages( + stages, """{ $sort : { _id : 1 } }""", """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }""" - }; + """{ $replaceRoot : { newRoot : "$_last" } }"""); - AssertStages(stages, expectedStages); result.Should().NotBeNull(); result.Id.Should().Be(3); } [Fact] - public void LastOrDefault_with_no_documents_should_work() + public void LastOrDefault_with_no_matching_documents_should_work() { var collection = Fixture.Collection; var queryable = collection.AsQueryable().Where(t => t.Id > 5); @@ -104,13 +95,12 @@ public void LastOrDefault_with_no_documents_should_work() var result = queryable.LastOrDefault(); var stages = queryable.GetMongoQueryProvider().LoggedStages; - var expectedStages = new[] { + AssertStages( + stages, """{ $match : { _id : { $gt : 5 } }}""", """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }""" - }; + """{ $replaceRoot : { newRoot : "$_last" } }"""); - AssertStages(stages, expectedStages); result.Should().BeNull(); } @@ -123,20 +113,19 @@ public void LastOrDefaultWithPredicate_should_work() var result = queryable.LastOrDefault(t => t.Id > 1); var stages = queryable.GetMongoQueryProvider().LoggedStages; - var expectedStages = new[] { + AssertStages( + stages, """{ $sort : { _id : 1 } }""", """{ $match : { _id : { $gt : 1 } }}""", """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }""" - }; + """{ $replaceRoot : { newRoot : "$_last" } }"""); - AssertStages(stages, expectedStages); result.Should().NotBeNull(); result.Id.Should().Be(3); } [Fact] - public void LastOrDefaultWithPredicate_and_null_return_should_work() + public void LastOrDefaultWithPredicate_with_no_matching_documents_should_work() { var collection = Fixture.Collection; var queryable = collection.AsQueryable(); @@ -144,13 +133,12 @@ public void LastOrDefaultWithPredicate_and_null_return_should_work() var result = queryable.LastOrDefault(t => t.Id > 4); var stages = queryable.GetMongoQueryProvider().LoggedStages; - var expectedStages = new[] { + AssertStages( + stages, """{ $match : { _id : { $gt : 4 } }}""", """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }""" - }; + """{ $replaceRoot : { newRoot : "$_last" } }"""); - AssertStages(stages, expectedStages); result.Should().BeNull(); } @@ -163,19 +151,18 @@ public void LastWithPredicate_should_work() var result = queryable.Last(t => t.Id > 1); var stages = queryable.GetMongoQueryProvider().LoggedStages; - var expectedStages = new[] { + AssertStages( + stages, """{ $sort : { _id : 1 } }""", """{ $match : { _id : { $gt : 1 } }}""", """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }""" - }; + """{ $replaceRoot : { newRoot : "$_last" } }"""); - AssertStages(stages, expectedStages); - Assert.Equal(3, result.Id); + result.Id.Should().Be(3); } [Fact] - public void LastWithPredicate_with_no_documents_should_throw() + public void LastWithPredicate_with_no_matching_documents_should_throw() { var collection = Fixture.Collection; var queryable = collection.AsQueryable(); @@ -183,13 +170,12 @@ public void LastWithPredicate_with_no_documents_should_throw() var exception = Record.Exception(() => queryable.Last(t => t.Id > 4)); var stages = queryable.GetMongoQueryProvider().LoggedStages; - var expectedStages = new[] { + AssertStages( + stages, """{ $match : { _id : { $gt : 4 } }}""", """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }""" - }; + """{ $replaceRoot : { newRoot : "$_last" } }"""); - AssertStages(stages, expectedStages); exception.Should().BeOfType<InvalidOperationException>(); exception.Message.Should().Contain("Sequence contains no elements"); } From 93c29896f16a99e967596085f5a4149185e7e67c Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 2 Apr 2025 11:07:17 +0200 Subject: [PATCH 7/7] Simplified quotes. --- ...tMethodToExecutableQueryTranslatorTests.cs | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs index 22871fadccd..28ee60042a4 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs @@ -41,9 +41,9 @@ public void Last_should_work() AssertStages( stages, - """{ $sort : { _id : 1 } }""", - """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }"""); + "{ $sort : { _id : 1 } }", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); result.Id.Should().Be(3); } @@ -59,9 +59,9 @@ public void Last_with_no_matching_documents_should_throw() AssertStages( stages, - """{ $match : { _id : { $gt : 5 } }}""", - """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }"""); + "{ $match : { _id : { $gt : 5 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); exception.Should().BeOfType<InvalidOperationException>(); exception.Message.Should().Contain("Sequence contains no elements"); @@ -78,9 +78,9 @@ public void LastOrDefault_should_work() AssertStages( stages, - """{ $sort : { _id : 1 } }""", - """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }"""); + "{ $sort : { _id : 1 } }", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); result.Should().NotBeNull(); result.Id.Should().Be(3); @@ -97,9 +97,9 @@ public void LastOrDefault_with_no_matching_documents_should_work() AssertStages( stages, - """{ $match : { _id : { $gt : 5 } }}""", - """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }"""); + "{ $match : { _id : { $gt : 5 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); result.Should().BeNull(); } @@ -115,10 +115,10 @@ public void LastOrDefaultWithPredicate_should_work() AssertStages( stages, - """{ $sort : { _id : 1 } }""", - """{ $match : { _id : { $gt : 1 } }}""", - """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }"""); + "{ $sort : { _id : 1 } }", + "{ $match : { _id : { $gt : 1 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); result.Should().NotBeNull(); result.Id.Should().Be(3); @@ -135,9 +135,9 @@ public void LastOrDefaultWithPredicate_with_no_matching_documents_should_work() AssertStages( stages, - """{ $match : { _id : { $gt : 4 } }}""", - """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }"""); + "{ $match : { _id : { $gt : 4 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); result.Should().BeNull(); } @@ -153,10 +153,10 @@ public void LastWithPredicate_should_work() AssertStages( stages, - """{ $sort : { _id : 1 } }""", - """{ $match : { _id : { $gt : 1 } }}""", - """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }"""); + "{ $sort : { _id : 1 } }", + "{ $match : { _id : { $gt : 1 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); result.Id.Should().Be(3); } @@ -172,9 +172,9 @@ public void LastWithPredicate_with_no_matching_documents_should_throw() AssertStages( stages, - """{ $match : { _id : { $gt : 4 } }}""", - """{ $group : { _id : null, _last : { $last : "$$ROOT" } } }""", - """{ $replaceRoot : { newRoot : "$_last" } }"""); + "{ $match : { _id : { $gt : 4 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); exception.Should().BeOfType<InvalidOperationException>(); exception.Message.Should().Contain("Sequence contains no elements");