Skip to content

Conversation

@adelinowona
Copy link
Contributor

No description provided.

@adelinowona adelinowona requested a review from a team as a code owner November 18, 2025 17:04
@adelinowona adelinowona added the improvement Optimizations or refactoring (no new features or fixes). label Nov 18, 2025
@adelinowona adelinowona requested review from rstam and removed request for ajcvickers November 18, 2025 17:04
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR optimizes LINQ translation for First() and FirstOrDefault() methods with predicates by adding a limit: 1 parameter to the $filter operator in the generated MongoDB aggregation pipeline. This optimization prevents MongoDB from scanning the entire array once a matching element is found, improving query performance.

Key changes:

  • Modified the translator to include limit: 1 in $filter operations when translating First() and FirstOrDefault() with predicates
  • Refactored to use a shared isFirstMethod flag for consistency across conditional logic
  • Updated all test assertions to expect the new limit: 1 parameter in generated aggregation stages

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.

Show a summary per file
File Description
FirstOrLastMethodToAggregationExpressionTranslator.cs Added logic to set limit: 1 for $filter when translating First/FirstOrDefault with predicates, and refactored conditional checks to use isFirstMethod flag
FirstOrLastMethodToAggregationExpressionTranslatorTests.cs Updated test assertions to verify limit: 1 is included in $filter operations
CSharp5779Tests.cs Updated test assertions for dictionary operations to expect limit: 1 in generated stages; also fixed a missing comma in line 95
CSharp5258Tests.cs Updated test assertion and removed extra space in expected stage output
CSharp4048Tests.cs Updated test assertions for IGrouping operations and fixed missing comma in line 121
CSharp3524Tests.cs Updated test assertion for SelectMany to expect limit: 1

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@rstam rstam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small change requested.

var expectedStages = new[]
{
"{ $group : { _id : '$_id', _elements : { $push: '$X' } } }",
"{ $project : { _id : '$_id' Result : { $arrayElemAt : ['$_elements', 0] } } }",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How was this test not failing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea. I just happened to catch it.

var sourceAst = sourceTranslation.Ast;
var itemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer);

var isFirstMethod = method.Name.StartsWith("First");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general we prefer to test which method it is like this:

var isFirstMethod = method.IsOneOf(__firstMethods);

It is less likely to have false matches than string matching (partial or otherwise).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it this way since I reasoned that if we passed the initial if statement:
if (method.IsOneOf(__firstOrLastMethods)) then we already know that the method has matched one of the first methods so no need to test the methods again.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statement you make is true, but even so we prefer to stick to method.IsOneOf instead of string matching.

The only time we don't use method.IsOneOf is when we are translating a method that might be implemented by classes we don't even know about (e.g. CompareTo).

@adelinowona adelinowona requested a review from rstam November 19, 2025 00:14
var sourceAst = sourceTranslation.Ast;
var itemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer);

var isFirstMethod = method.Name.StartsWith("First");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statement you make is true, but even so we prefer to stick to method.IsOneOf instead of string matching.

The only time we don't use method.IsOneOf is when we are translating a method that might be implemented by classes we don't even know about (e.g. CompareTo).

if (isFirstMethod)
{
var compatibilityLevel = context.TranslationOptions.CompatibilityLevel;
if (compatibilityLevel is >= ServerVersion.Server60)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather this line say:

if (Feature.FilterLimit.IsSupported(compatibilityLevel.ToWireVersion()))

"{ $sort : { _id : 1 } }"
};

var expectedStages = FilterLimitIsSupported
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I almost find the longer version more readable:

var expectedStages = Feature.FilterLimit.IsSupported(CoreTestConfiguration.MaxWireVersion)

because I don't have to chase down how FilterLimitIsSupported is computed.

In some other files you still use the long form.

Copy link
Contributor Author

@adelinowona adelinowona Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather keep the FilterLimitIsSupported. I didn't want to have to keep repeating the longer form everywhere especially when it is the same information that we can get once and just reuse everywhere.

In some other files. there is only one use case, which is why I just used the longer form directly.

var parameterSymbol = context.CreateSymbol(parameterExpression, itemSerializer, isCurrent: false);
var predicateTranslation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, predicateLambda, parameterSymbol);

// The $filter limit parameter was introduced in MongoDB 6.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment becomes redundant and can be removed if you accept the change recommended on line 112 as then the code becomes self documenting.

@adelinowona adelinowona requested a review from rstam November 20, 2025 04:50
Copy link
Contributor

@rstam rstam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@adelinowona adelinowona merged commit 2d60432 into mongodb:main Nov 21, 2025
34 of 36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Optimizations or refactoring (no new features or fixes).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants