Skip to content

Commit a3283e3

Browse files
committed
CSHARP-4763: Support client side projections.
1 parent 10658fe commit a3283e3

File tree

52 files changed

+1945
-389
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1945
-389
lines changed

src/MongoDB.Driver/AggregateOptions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ public class AggregateOptions
3737
private ExpressionTranslationOptions _translationOptions;
3838
private bool? _useCursor;
3939

40+
// implicit conversions
41+
/// <summary>
42+
/// Creates an AggregateOptions instance with the specified translation options.
43+
/// </summary>
44+
/// <param name="translationOptions">The translation options.</param>
45+
/// <returns>An AggregateOptions instance.</returns>
46+
public static implicit operator AggregateOptions(ExpressionTranslationOptions translationOptions) =>
47+
new AggregateOptions { TranslationOptions = translationOptions };
48+
4049
// properties
4150
/// <summary>
4251
/// Gets or sets a value indicating whether to allow disk use.
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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.Linq.Expressions;
18+
using MongoDB.Bson.Serialization;
19+
using MongoDB.Bson.Serialization.Serializers;
20+
using MongoDB.Driver.Core.Misc;
21+
22+
namespace MongoDB.Driver
23+
{
24+
internal interface IClientSideProjectionDeserializer
25+
{
26+
}
27+
28+
internal static class ClientSideProjectionDeserializer
29+
{
30+
public static IBsonSerializer Create(
31+
IBsonSerializer inputSerializer,
32+
LambdaExpression projector)
33+
{
34+
var inputType = inputSerializer.ValueType;
35+
var projectionType = projector.ReturnType;
36+
var serializerType = typeof(ClientSideProjectionDeserializer<,>).MakeGenericType(inputType, projectionType);
37+
var projectorDelegate = projector.Compile();
38+
return (IBsonSerializer)Activator.CreateInstance(serializerType, inputSerializer, projectorDelegate);
39+
}
40+
}
41+
42+
/// <summary>
43+
/// A deserializer for doing client side projections.
44+
/// </summary>
45+
/// <typeparam name="TInput">The type of the input.</typeparam>
46+
/// <typeparam name="TProjection">The type of the projection.</typeparam>
47+
public sealed class ClientSideProjectionDeserializer<TInput, TProjection> : SerializerBase<TProjection>, IClientSideProjectionDeserializer
48+
{
49+
private readonly Func<TInput, TProjection> _projector;
50+
private readonly IBsonSerializer<TInput> _inputSerializer;
51+
52+
/// <summary>
53+
/// Initializes a new instance of the <see cref="ClientSideProjectionDeserializer{TInput, TProjection}"/> class.
54+
/// </summary>
55+
/// <param name="inputSerializer">The input serializer.</param>
56+
/// <param name="projector">The client side projector.</param>
57+
public ClientSideProjectionDeserializer(
58+
IBsonSerializer<TInput> inputSerializer,
59+
Func<TInput, TProjection> projector)
60+
{
61+
_inputSerializer = Ensure.IsNotNull(inputSerializer, nameof(inputSerializer));
62+
_projector = Ensure.IsNotNull(projector, nameof(projector));
63+
}
64+
65+
/// <inheritdoc/>
66+
public override TProjection Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
67+
{
68+
var document = _inputSerializer.Deserialize(context);
69+
return _projector(document);
70+
}
71+
}
72+
}

src/MongoDB.Driver/ExpressionTranslationOptions.cs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
* See the License for the specific language governing permissions and
1313
* limitations under the License.
14-
*
14+
*
1515
*/
1616

1717
namespace MongoDB.Driver
@@ -26,30 +26,51 @@ public sealed class ExpressionTranslationOptions
2626
/// </summary>
2727
public ServerVersion? CompatibilityLevel { get; set; }
2828

29+
/// <summary>
30+
/// Gets or sets whether client side projections are enabled.
31+
/// </summary>
32+
public bool? EnableClientSideProjections { get; set; }
33+
2934
/// <inheritdoc/>
3035
public override bool Equals(object obj)
3136
{
3237
if (object.ReferenceEquals(obj, null)) { return false; }
3338
if (object.ReferenceEquals(this, obj)) { return true; }
3439
return
35-
base.Equals(obj) &&
40+
GetType().Equals(obj.GetType()) &&
3641
obj is ExpressionTranslationOptions other &&
37-
CompatibilityLevel.Equals(other.CompatibilityLevel);
42+
CompatibilityLevel.Equals(other.CompatibilityLevel) &&
43+
EnableClientSideProjections.Equals(other.EnableClientSideProjections);
3844
}
3945

4046
/// <inheritdoc/>
4147
public override int GetHashCode() => 0;
4248

4349
/// <inheritdoc/>
44-
public override string ToString() => $"{{ CompatibilityLevel = {CompatibilityLevel} }}";
50+
public override string ToString()
51+
{
52+
var compatibilityLevel = CompatibilityLevel.HasValue ? CompatibilityLevel.ToString() : "null";
53+
var enableClientSideProjections = EnableClientSideProjections.HasValue ? EnableClientSideProjections.ToString() : "null";
54+
return $"{{ CompatibilityLevel = {compatibilityLevel}, EnableClientSideProjections = {enableClientSideProjections} }}";
55+
}
4556
}
4657

4758
internal static class ExpressionTranslationOptionsExtensions
4859
{
4960
public static ExpressionTranslationOptions AddMissingOptionsFrom(this ExpressionTranslationOptions translationOptions, ExpressionTranslationOptions from)
5061
{
51-
// in the future ExpressionTranslationOptions might have more properties
52-
return (translationOptions?.CompatibilityLevel).HasValue ? translationOptions : from;
62+
if (translationOptions == null || from == null)
63+
{
64+
return translationOptions ?? from;
65+
}
66+
else
67+
{
68+
return new ExpressionTranslationOptions
69+
{
70+
CompatibilityLevel = translationOptions.CompatibilityLevel ?? from.CompatibilityLevel,
71+
EnableClientSideProjections = translationOptions.EnableClientSideProjections ?? from.EnableClientSideProjections
72+
};
73+
}
5374
}
5475
}
5576
}

src/MongoDB.Driver/FindFluent.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ public override IFindFluent<TDocument, TNewProjection> Project<TNewProjection>(P
144144
ShowRecordId = _options.ShowRecordId,
145145
Skip = _options.Skip,
146146
Sort = _options.Sort,
147+
TranslationOptions = _options.TranslationOptions
147148
};
148149
return new FindFluent<TDocument, TNewProjection>(_session, _collection, _filter, newOptions);
149150
}

src/MongoDB.Driver/FindOptions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public abstract class FindOptionsBase
4141
private bool? _oplogReplay;
4242
private bool? _returnKey;
4343
private bool? _showRecordId;
44+
private ExpressionTranslationOptions _translationOptions;
4445

4546
// constructors
4647
/// <summary>
@@ -196,6 +197,15 @@ public bool? ShowRecordId
196197
get { return _showRecordId; }
197198
set { _showRecordId = value; }
198199
}
200+
201+
/// <summary>
202+
/// Gets or sets the translation options.
203+
/// </summary>
204+
public ExpressionTranslationOptions TranslationOptions
205+
{
206+
get { return _translationOptions; }
207+
set { _translationOptions = value; }
208+
}
199209
}
200210

201211
/// <summary>

src/MongoDB.Driver/IMongoCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,7 @@ private static IFindFluent<TDocument, TDocument> FindHelper<TDocument>(IClientSe
12001200
#pragma warning restore 618
12011201
ReturnKey = options.ReturnKey,
12021202
ShowRecordId = options.ShowRecordId,
1203+
TranslationOptions = options.TranslationOptions
12031204
};
12041205
}
12051206

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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.Linq.Expressions;
18+
using System.Reflection;
19+
using MongoDB.Bson.Serialization;
20+
using MongoDB.Driver.Linq.Linq3Implementation.Ast;
21+
22+
namespace MongoDB.Driver.Linq.Linq3Implementation.Misc
23+
{
24+
internal static class ClientSideProjectionHelper
25+
{
26+
// public static methods
27+
public static void ThrowIfClientSideProjection(
28+
Expression expression,
29+
AstPipeline pipeline,
30+
MethodInfo method)
31+
{
32+
if (pipeline.OutputSerializer is IClientSideProjectionDeserializer)
33+
{
34+
throw new ExpressionNotSupportedException(expression, because: $"{method.Name} cannot follow a client side projection");
35+
}
36+
}
37+
38+
public static void ThrowIfClientSideProjection(
39+
Expression expression,
40+
AstPipeline pipeline,
41+
MethodInfo method,
42+
string methodOverload)
43+
{
44+
if (pipeline.OutputSerializer is IClientSideProjectionDeserializer)
45+
{
46+
throw new ExpressionNotSupportedException(expression, because: $"{method.Name} {methodOverload} cannot follow a client side projection");
47+
}
48+
}
49+
50+
public static void ThrowIfClientSideProjection(
51+
IBsonSerializer inputSerializer,
52+
string stageName)
53+
{
54+
if (inputSerializer is IClientSideProjectionDeserializer)
55+
{
56+
throw new NotSupportedException($"A {stageName} stage cannot follow a client side projection");
57+
}
58+
}
59+
}
60+
}

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/AllMethodToExecutableQueryTranslator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public static ExecutableQuery<TDocument, bool> Translate<TDocument>(MongoQueryPr
4747
{
4848
var sourceExpression = arguments[0];
4949
var pipeline = ExpressionToPipelineTranslator.Translate(context, sourceExpression);
50+
ClientSideProjectionHelper.ThrowIfClientSideProjection(expression, pipeline, method);
5051

5152
var predicateLambda = ExpressionHelper.UnquoteLambda(arguments[1]);
5253
var predicateFilter = ExpressionToFilterTranslator.TranslateLambda(context, predicateLambda, parameterSerializer: pipeline.OutputSerializer, asRoot: true);

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/AnyMethodToExecutableQueryTranslator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public static ExecutableQuery<TDocument, bool> Translate<TDocument>(MongoQueryPr
7070

7171
if (method.IsOneOf(__anyWithPredicateMethods))
7272
{
73+
ClientSideProjectionHelper.ThrowIfClientSideProjection(expression, pipeline, method, "with a predicate");
74+
7375
var predicateLambda = ExpressionHelper.UnquoteLambda(arguments[1]);
7476
var predicateFilter = ExpressionToFilterTranslator.TranslateLambda(context, predicateLambda, parameterSerializer: pipeline.OutputSerializer, asRoot: true);
7577

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/AverageMethodToExecutableQueryTranslator.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ public static ExecutableQuery<TDocument, TOutput> Translate<TDocument>(MongoQuer
120120
{
121121
var sourceExpression = arguments[0];
122122
var pipeline = ExpressionToPipelineTranslator.Translate(context, sourceExpression);
123-
var sourceSerializer = pipeline.OutputSerializer;
123+
ClientSideProjectionHelper.ThrowIfClientSideProjection(expression, pipeline, method);
124124

125+
var sourceSerializer = pipeline.OutputSerializer;
125126
AstExpression valueExpression;
126127
if (method.IsOneOf(__averageWithSelectorMethods))
127128
{

0 commit comments

Comments
 (0)