CSHARP-5847: Support Select/SelectMany/Where index overloads in LINQ provider#1949
CSHARP-5847: Support Select/SelectMany/Where index overloads in LINQ provider#1949adelinowona merged 7 commits intomongodb:mainfrom
Conversation
There was a problem hiding this comment.
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$mapwitharrayIndexAs. - Translate
Where((item, index) => ...)to$filterwitharrayIndexAs. - Translate
SelectMany((item, index) => ...)via$map(witharrayIndexAs) +$reduce/$concatArrays, and add coverage/tests plus a newFeature.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.
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
| /// Gets the arrayIndexAs feature for $map, $filter and $reduce. | |
| /// Gets the arrayIndexAs feature for $map and $filter. |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Shouldn't we use the serializer resolved by SerializerFinder?
There was a problem hiding this comment.
Found the answer in tests. Cool!
| { | ||
| internal sealed class AstFilterExpression : AstExpression | ||
| { | ||
| private readonly string _arrayIndexAs; |
There was a problem hiding this comment.
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?
| [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>)], |
There was a problem hiding this comment.
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.
BorisDog
left a comment
There was a problem hiding this comment.
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) |
| { | ||
| internal sealed class AstFilterExpression : AstExpression | ||
| { | ||
| private readonly string _arrayIndexAs; |
No description provided.