Skip to content

CSHARP-5847: Support Select/SelectMany/Where index overloads in LINQ provider#1949

Merged
adelinowona merged 7 commits intomongodb:mainfrom
adelinowona:csharp5847
Apr 16, 2026
Merged

CSHARP-5847: Support Select/SelectMany/Where index overloads in LINQ provider#1949
adelinowona merged 7 commits intomongodb:mainfrom
adelinowona:csharp5847

Conversation

@adelinowona
Copy link
Copy Markdown
Contributor

No description provided.

@adelinowona adelinowona requested a review from sanych-sun April 14, 2026 15:56
@adelinowona adelinowona requested a review from a team as a code owner April 14, 2026 15:56
@adelinowona adelinowona added the feature Adds new user-facing functionality. label Apr 14, 2026
Copilot AI review requested due to automatic review settings April 14, 2026 15:56
Copy link
Copy Markdown
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

Adds LINQ3 translation support for the index-taking overloads of Select, SelectMany, and Where by emitting aggregation expressions that use the server-side arrayIndexAs capability, along with integration tests validating the generated stages and results.

Changes:

  • Translate Select((item, index) => ...) to $map with arrayIndexAs.
  • Translate Where((item, index) => ...) to $filter with arrayIndexAs.
  • Translate SelectMany((item, index) => ...) via $map (with arrayIndexAs) + $reduce/$concatArrays, and add coverage/tests plus a new Feature.ArrayIndexAs.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/MongoDB.Driver/Linq/Linq3Implementation/Translators/.../WhereMethodToAggregationExpressionTranslator.cs Adds translation for index-based Where using $filter.arrayIndexAs.
src/MongoDB.Driver/Linq/Linq3Implementation/Translators/.../SelectMethodToAggregationExpressionTranslator.cs Adds translation for index-based Select using $map.arrayIndexAs.
src/MongoDB.Driver/Linq/Linq3Implementation/Translators/.../SelectManyMethodToAggregationExpressionTranslator.cs Adds translation for index-based SelectMany using $map.arrayIndexAs and flattens with $reduce.
src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderVisitMethodCall.cs Deduces serializers for the new index-based method overloads (including int index).
src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/EnumerableOrQueryableMethod.cs Registers the new method sets for the index-taking overloads.
src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs Updates AST visiting for $map to include ArrayIndexAs.
src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstMapExpression.cs Extends $map AST node to render arrayIndexAs.
src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstFilterExpression.cs Extends $filter AST node to render arrayIndexAs.
src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs Extends factory methods for $map/$filter to accept arrayIndexAs.
src/MongoDB.Driver/Core/Misc/Feature.cs Introduces Feature.ArrayIndexAs.
tests/.../WhereMethodToAggregationExpressionTranslatorTests.cs Adds integration tests for index-based Where translation.
tests/.../SelectMethodToAggregationExpressionTranslatorTests.cs Adds integration tests for index-based Select translation.
tests/.../SelectManyMethodToAggregationExpressionTranslatorTests.cs New integration tests for index-based SelectMany translation.

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

Comment on lines +97 to +104
var ast = AstExpression.Filter(
sourceAst,
predicateTranslation.Ast,
@as: itemSymbol.Var.Name,
arrayIndexAs: indexSymbol.Var.Name);

var resultSerializer = context.GetSerializer(expression);
return new TranslatedExpression(expression, ast, resultSerializer);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The index-based Where translation uses $filter with arrayIndexAs but does not validate Feature.ArrayIndexAs support for the current compatibility level. Please add an explicit feature gate (similar to Feature.FilterLimit usage elsewhere) so that on unsupported servers/compatibility settings the driver fails early with a clear exception instead of sending an invalid aggregation pipeline.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is intentional. The driver doesn't generally gate LINQ translations on server version — we translate the query and let the server reject it if the feature isn't supported. The tests are gated with RequireServer.Check().Supports(Feature.ArrayIndexAs) to avoid running against incompatible servers.

Comment on lines +68 to +75
var selectorLambda = ExpressionHelper.UnquoteLambdaIfQueryableMethod(method, arguments[1]);
var itemParameter = selectorLambda.Parameters[0];
var indexParameter = selectorLambda.Parameters[1];
var itemSymbol = context.CreateSymbol(itemParameter, context.GetSerializer(itemParameter));
var indexSymbol = context.CreateSymbol(indexParameter, context.GetSerializer(indexParameter));
var selectorContext = context.WithSymbols(itemSymbol, indexSymbol);
var selectorTranslation = ExpressionToAggregationExpressionTranslator.Translate(selectorContext, selectorLambda.Body);
var resultItemSerializer = ArraySerializerHelper.GetItemSerializer(selectorTranslation.Serializer);
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

In the index overload branch, itemSymbol is created via context.GetSerializer(itemParameter) instead of deriving the selector parameter serializer from sourceTranslation.Serializer (as is done in the non-index SelectMany branch). For consistency and correctness (especially with wrapped/nested sources), consider unwrapping IWrappedValueSerializer if needed and using ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer) for the item parameter, and Int32Serializer.Instance for the index parameter.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is intentional. The serializer finder (SerializerFinderVisitMethodCall) pre-deduces the correct serializer for the item and index parameters before the translator runs.

context.GetSerializer(itemParameter) retrieves that pre-deduced serializer, so the result is the same as manually deriving it from the source serializer. This approach is consistent with how the serializer finder is designed to work.

public static Feature AggregateUnionWith => __aggregateUnionWith;

/// <summary>
/// Gets the arrayIndexAs feature for $map, $filter and $reduce.
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The XML doc comment for Feature.ArrayIndexAs says it's for "$map, $filter and $reduce", but arrayIndexAs is used by $map and $filter (and is not a parameter of $reduce). Please adjust the comment to avoid misleading readers about server feature coverage.

Suggested change
/// Gets the arrayIndexAs feature for $map, $filter and $reduce.
/// Gets the arrayIndexAs feature for $map and $filter.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The XML comment is correct as written — arrayIndexAs is also available on $reduce. Support for it is tracked separately in another ticket - CSHARP-5973


var ienumerableSerializer = IEnumerableSerializer.Create(itemSerializer);
return new TranslatedExpression(expression, ast, ienumerableSerializer);
var serializer = NestedAsQueryableSerializer.CreateIEnumerableOrNestedAsQueryableSerializer(expression.Type, itemSerializer);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed from IEnumerableSerializer.Create(itemSerializer) to NestedAsQueryableSerializer.CreateIEnumerableOrNestedAsQueryableSerializer(expression.Type, itemSerializer) to correctly handle both IEnumerable and IQueryable source types. This aligns with how Select and the new index overload already create their result serializers.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't we use the serializer resolved by SerializerFinder?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Found the answer in tests. Cool!

{
internal sealed class AstFilterExpression : AstExpression
{
private readonly string _arrayIndexAs;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

arrayIndexAs is typed as string here (not AstVarExpression) to match the existing convention for AstFilterExpression.As, which is also a string. AstMapExpression uses AstVarExpression for both its As and ArrayIndexAs fields — each class follows its own established pattern. Not sure if I should change it to use AstVarExpression thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm OK with string name.

[TestHelpers.MakeLambda((MyModel model) => model.FloatItems.Percentile(new double[] { 0.5 })), typeof(ArraySerializer<float>)],
[TestHelpers.MakeLambda((MyModel model) => model.FloatItems.Percentile(x => x * 2, new double[] { 0.5 })), typeof(ArraySerializer<float>)],

[TestHelpers.MakeLambda((MyModel model) => model.Items.Select(x => x + 1)), typeof(IEnumerableSerializer<int>)],
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

IQueryable variants for Select/SelectMany/Where (with and without index) are intentionally omitted. The serializer finder currently produces IQueryableSerializer for nested AsQueryable() expressions, but the translators need NestedAsQueryableSerializer. Adding IQueryable test cases here would assert on the wrong serializer type. Tracked in CSHARP-5976.

Copy link
Copy Markdown
Contributor

@BorisDog BorisDog left a comment

Choose a reason for hiding this comment

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

Looks good. But we need a sign off from someone with a deeper LINQ expertise.

AstExpression limit)
{
if (input == _input && cond == _cond)
if (input == _input && cond == _cond && limit == _limit)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good catch!

Copy link
Copy Markdown
Member

@sanych-sun sanych-sun left a comment

Choose a reason for hiding this comment

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

LGTM

{
internal sealed class AstFilterExpression : AstExpression
{
private readonly string _arrayIndexAs;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm OK with string name.

@adelinowona adelinowona merged commit c24ee00 into mongodb:main Apr 16, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Adds new user-facing functionality.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants