Skip to content

Commit 836aa60

Browse files
committed
CSHARP-5321: Optimize client-side projections to perform as much as possible of the projection on the server.
1 parent c2bec81 commit 836aa60

File tree

19 files changed

+587
-95
lines changed

19 files changed

+587
-95
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using MongoDB.Bson;
18+
using MongoDB.Bson.IO;
19+
using MongoDB.Bson.Serialization;
20+
using MongoDB.Bson.Serialization.Serializers;
21+
22+
namespace MongoDB.Driver
23+
{
24+
internal static class ClientSideProjectionSnippetsDeserializer
25+
{
26+
public static IBsonSerializer Create(
27+
Type projectionType,
28+
IBsonSerializer[] snippetDeserializers,
29+
Delegate projector)
30+
{
31+
var deserializerType = typeof(ClientSideProjectionSnippetsDeserializer<>).MakeGenericType(projectionType);
32+
return (IBsonSerializer)Activator.CreateInstance(deserializerType, [snippetDeserializers, projector]);
33+
}
34+
}
35+
36+
internal sealed class ClientSideProjectionSnippetsDeserializer<TProjection> : SerializerBase<TProjection>, IClientSideProjectionDeserializer
37+
{
38+
private readonly IBsonSerializer[] _snippetDeserializers;
39+
private readonly Func<object[], TProjection> _projector;
40+
41+
public ClientSideProjectionSnippetsDeserializer(IBsonSerializer[] snippetDeserializers, Func<object[], TProjection> projector)
42+
{
43+
_snippetDeserializers = snippetDeserializers;
44+
_projector = projector;
45+
}
46+
47+
public override TProjection Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
48+
{
49+
var snippets = DeserializeSnippets(context);
50+
return _projector(snippets);
51+
}
52+
53+
private object[] DeserializeSnippets(BsonDeserializationContext context)
54+
{
55+
var reader = context.Reader;
56+
57+
reader.ReadStartDocument();
58+
reader.ReadName("_snippets");
59+
reader.ReadStartArray();
60+
var snippets = new object[_snippetDeserializers.Length];
61+
var i = 0;
62+
while (reader.ReadBsonType() != BsonType.EndOfDocument)
63+
{
64+
if (i >= _snippetDeserializers.Length)
65+
{
66+
throw new BsonSerializationException($"Expected {_snippetDeserializers.Length} snippets but found more than that.");
67+
}
68+
snippets[i] = _snippetDeserializers[i].Deserialize(context);
69+
i++;
70+
}
71+
if (i != _snippetDeserializers.Length)
72+
{
73+
throw new BsonSerializationException($"Expected {_snippetDeserializers.Length} snippets but found {i}.");
74+
}
75+
reader.ReadEndArray();
76+
reader.ReadEndDocument();
77+
78+
return snippets;
79+
}
80+
}
81+
}

src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ExpressionIsReferencedVisitor.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Misc
2020
{
2121
internal class ExpressionIsReferencedVisitor : ExpressionVisitor
2222
{
23+
#region static
24+
public static bool IsReferenced(Expression node, Expression expression)
25+
{
26+
var visitor = new ExpressionIsReferencedVisitor(expression);
27+
visitor.Visit(node);
28+
return visitor.ExpressionIsReferenced;
29+
}
30+
#endregion
31+
2332
private readonly Expression _expression;
2433
private bool _expressionIsReferenced;
2534

src/MongoDB.Driver/Linq/Linq3Implementation/Misc/LambdaExpressionExtensions.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ internal static class LambdaExpressionExtensions
2626
{
2727
public static bool LambdaBodyReferencesParameter(this LambdaExpression lambda, ParameterExpression parameter)
2828
{
29-
var visitor = new ExpressionIsReferencedVisitor(parameter);
30-
visitor.Visit(lambda.Body);
31-
return visitor.ExpressionIsReferenced;
29+
return ExpressionIsReferencedVisitor.IsReferenced(lambda.Body, parameter);
3230
}
3331

3432
public static string TranslateToDottedFieldName(this LambdaExpression fieldSelectorLambda, TranslationContext context, IBsonSerializer parameterSerializer)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using System.Linq.Expressions;
20+
using System.Reflection;
21+
using MongoDB.Bson.Serialization;
22+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
23+
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
24+
using ExpressionVisitor = System.Linq.Expressions.ExpressionVisitor;
25+
26+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators
27+
{
28+
internal class ClientSideProjectionRewriter: ExpressionVisitor
29+
{
30+
#region static
31+
private readonly static MethodInfo[] __orderByMethods =
32+
[
33+
EnumerableMethod.OrderBy,
34+
EnumerableMethod.OrderByDescending,
35+
QueryableMethod.OrderBy,
36+
QueryableMethod.OrderByDescending
37+
];
38+
39+
private readonly static MethodInfo[] __thenByMethods =
40+
[
41+
EnumerableMethod.ThenBy,
42+
EnumerableMethod.ThenByDescending,
43+
QueryableMethod.ThenBy,
44+
QueryableMethod.ThenByDescending
45+
];
46+
47+
public static (TranslatedExpression[], LambdaExpression) RewriteProjection(TranslationContext context, LambdaExpression projectionLambda, IBsonSerializer sourceSerializer)
48+
{
49+
var rootParameter = projectionLambda.Parameters.Single();
50+
var rootSymbol = context.CreateRootSymbol(rootParameter, sourceSerializer);
51+
context = context.WithSymbol(rootSymbol);
52+
53+
var snippetsParameter = Expression.Parameter(typeof(object[]), "snippets");
54+
var projectionRewriter = new ClientSideProjectionRewriter(context, snippetsParameter);
55+
var rewrittenBody = projectionRewriter.Visit(projectionLambda.Body);
56+
var rewrittenLambda = Expression.Lambda(rewrittenBody, snippetsParameter);
57+
var snippetsArray = projectionRewriter.Snippets.ToArray();
58+
59+
return (snippetsArray, rewrittenLambda);
60+
}
61+
#endregion
62+
63+
private readonly TranslationContext _context;
64+
private readonly List<TranslatedExpression> _snippets = new();
65+
private readonly ParameterExpression _snippetsParameter;
66+
67+
private ClientSideProjectionRewriter(TranslationContext context, ParameterExpression snippetsParameter)
68+
{
69+
_context = context;
70+
_snippetsParameter = snippetsParameter;
71+
}
72+
73+
private List<TranslatedExpression> Snippets => _snippets;
74+
75+
public override Expression Visit(Expression node)
76+
{
77+
if (node == null)
78+
{
79+
return null;
80+
}
81+
82+
if (node.NodeType == ExpressionType.Constant)
83+
{
84+
return node; // don't make snippets for constants
85+
}
86+
87+
TranslatedExpression snippet;
88+
try
89+
{
90+
snippet = ExpressionToAggregationExpressionTranslator.Translate(_context, node);
91+
}
92+
catch
93+
{
94+
return base.Visit(node); // try to find smaller snippets below this node
95+
}
96+
97+
var snippetIndex = _snippets.Count;
98+
_snippets.Add(snippet);
99+
100+
var snippetReference = // (T)snippets[i]
101+
Expression.Convert(
102+
Expression.ArrayIndex(_snippetsParameter, Expression.Constant(snippetIndex)),
103+
snippet.Expression.Type);
104+
105+
return snippetReference;
106+
}
107+
108+
protected override Expression VisitMethodCall(MethodCallExpression node)
109+
{
110+
// don't split OrderBy/ThenBy across the client/server boundary
111+
if (node.Method.IsOneOf(__thenByMethods))
112+
{
113+
return VisitThenBy(node);
114+
}
115+
116+
return base.VisitMethodCall(node);
117+
}
118+
119+
private Expression VisitThenBy(MethodCallExpression node)
120+
{
121+
var arguments = node.Arguments;
122+
var sourceExpression = arguments[0];
123+
var keySelectorExpression = arguments[1];
124+
125+
if (sourceExpression is MethodCallExpression sourceMethodCallExpression)
126+
{
127+
var sourceMethod = sourceMethodCallExpression.Method;
128+
129+
if (sourceMethod.IsOneOf(__thenByMethods))
130+
{
131+
var rewrittenSourceExpression = VisitThenBy(sourceMethodCallExpression);
132+
return node.Update(node.Object, [rewrittenSourceExpression, keySelectorExpression]);
133+
}
134+
135+
if (sourceMethod.IsOneOf(__orderByMethods))
136+
{
137+
var rewrittenSourceExpression = VisitOrderBy(sourceMethodCallExpression);
138+
return node.Update(node.Object, [rewrittenSourceExpression, keySelectorExpression]);
139+
}
140+
}
141+
142+
throw new ArgumentException("ThenBy or ThenByDescending not preceded by OrderBy or OrderByDescending.", nameof(node));
143+
}
144+
145+
private Expression VisitOrderBy(MethodCallExpression node)
146+
{
147+
var arguments = node.Arguments;
148+
var sourceExpression = arguments[0];
149+
var keySelectorExpression = arguments[1];
150+
var rewrittenSourceExpression = Visit(sourceExpression);
151+
return node.Update(node.Object, [rewrittenSourceExpression, keySelectorExpression]);
152+
}
153+
}
154+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Linq;
17+
using System.Linq.Expressions;
18+
using MongoDB.Bson.Serialization;
19+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
20+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Stages;
21+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
22+
23+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators
24+
{
25+
internal static class ClientSideProjectionTranslator
26+
{
27+
public static (AstProjectStage, IBsonSerializer) CreateProjectSnippetsStage(
28+
TranslationContext context,
29+
LambdaExpression projectionLambda,
30+
IBsonSerializer sourceSerializer)
31+
{
32+
var (snippetsAst, snippetsProjectionDeserializer) = RewriteProjectionUsingSnippets(context, projectionLambda, sourceSerializer);
33+
if (snippetsAst == null)
34+
{
35+
return (null, snippetsProjectionDeserializer);
36+
}
37+
else
38+
{
39+
var snippetsTranslation = new TranslatedExpression(projectionLambda, snippetsAst, snippetsProjectionDeserializer);
40+
return ProjectionHelper.CreateProjectStage(snippetsTranslation);
41+
}
42+
}
43+
44+
private static (AstComputedDocumentExpression, IBsonSerializer) RewriteProjectionUsingSnippets(
45+
TranslationContext context,
46+
LambdaExpression projectionLambda,
47+
IBsonSerializer sourceSerializer)
48+
{
49+
var (snippets, rewrittenProjectionLamdba) = ClientSideProjectionRewriter.RewriteProjection(context, projectionLambda, sourceSerializer);
50+
51+
if (snippets.Length == 0 || snippets.Any(IsRoot))
52+
{
53+
var clientSideProjectionDeserializer = ClientSideProjectionDeserializer.Create(sourceSerializer, projectionLambda);
54+
return (null, clientSideProjectionDeserializer); // project directly off $$ROOT with no snippets
55+
}
56+
else
57+
{
58+
var snippetsComputedDocument = CreateSnippetsComputedDocument(snippets);
59+
var snippetDeserializers = snippets.Select(s => s.Serializer).ToArray();
60+
var rewrittenProjectionDelegate = rewrittenProjectionLamdba.Compile();
61+
var clientSideProjectionSnippetsDeserializer = ClientSideProjectionSnippetsDeserializer.Create(projectionLambda.ReturnType, snippetDeserializers, rewrittenProjectionDelegate);
62+
return (snippetsComputedDocument, clientSideProjectionSnippetsDeserializer);
63+
}
64+
65+
static bool IsRoot(TranslatedExpression snippet) => snippet.Ast.IsRootVar();
66+
}
67+
68+
private static AstComputedDocumentExpression CreateSnippetsComputedDocument(TranslatedExpression[] snippets)
69+
{
70+
var snippetsArray = AstExpression.ComputedArray(snippets.Select(s => s.Ast));
71+
var snippetsField = AstExpression.ComputedField("_snippets", snippetsArray);
72+
return (AstComputedDocumentExpression)AstExpression.ComputedDocument([snippetsField]);
73+
}
74+
}
75+
}

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/SelectMethodToPipelineTranslator.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
using System;
1717
using System.Linq.Expressions;
18+
using MongoDB.Bson.Serialization;
1819
using MongoDB.Driver.Linq.Linq3Implementation.Ast;
1920
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Stages;
2021
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
@@ -46,19 +47,21 @@ public static TranslatedPipeline Translate(TranslationContext context, MethodCal
4647
ClientSideProjectionHelper.ThrowIfClientSideProjection(expression, pipeline, method);
4748

4849
var sourceSerializer = pipeline.OutputSerializer;
50+
AstProjectStage projectStage;
51+
IBsonSerializer projectionSerializer;
4952
try
5053
{
5154
var selectorTranslation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, selectorLambda, sourceSerializer, asRoot: true);
52-
var (projectStage, projectionSerializer) = ProjectionHelper.CreateProjectStage(selectorTranslation);
53-
pipeline = pipeline.AddStage(projectStage, projectionSerializer);
55+
(projectStage, projectionSerializer) = ProjectionHelper.CreateProjectStage(selectorTranslation);
5456
}
5557
catch (ExpressionNotSupportedException) when (context.TranslationOptions?.EnableClientSideProjections ?? false)
5658
{
57-
var clientSideProjectionDeserializer = ClientSideProjectionDeserializer.Create(sourceSerializer, selectorLambda);
58-
pipeline = pipeline.WithNewOutputSerializer(clientSideProjectionDeserializer);
59+
(projectStage, projectionSerializer) = ClientSideProjectionTranslator.CreateProjectSnippetsStage(context, selectorLambda, sourceSerializer);
5960
}
6061

61-
return pipeline;
62+
return projectStage == null ?
63+
pipeline.WithNewOutputSerializer(projectionSerializer) : // project directly off $$ROOT with no $project stage
64+
pipeline.AddStage(projectStage, projectionSerializer);
6265
}
6366

6467
throw new ExpressionNotSupportedException(expression);

0 commit comments

Comments
 (0)