Skip to content

Commit 8dac726

Browse files
committed
CSHARP-5691: Non-deterministic functions should not be evaluated client-side
1 parent 17c8263 commit 8dac726

File tree

10 files changed

+366
-2
lines changed

10 files changed

+366
-2
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/ExtensionMethods/ExpressionExtensions.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@
1616
using System;
1717
using System.Linq;
1818
using System.Linq.Expressions;
19+
using System.Reflection;
1920
using MongoDB.Bson.Serialization;
21+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
22+
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
2023

2124
namespace MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods
2225
{
2326
internal static class ExpressionExtensions
2427
{
28+
private readonly static MethodInfo[] __nonDeterministicMethods =
29+
[
30+
GuidMethod.NewGuid,
31+
RandomMethod.Next
32+
];
33+
34+
private readonly static PropertyInfo[] __nonDeterministicProperties =
35+
[
36+
DateTimeProperty.Now,
37+
DateTimeProperty.Today,
38+
DateTimeProperty.UtcNow,
39+
DateTimeOffsetProperty.Now,
40+
DateTimeOffsetProperty.UtcNow
41+
];
2542
public static object Evaluate(this Expression expression)
2643
{
2744
if (expression is ConstantExpression constantExpression)
@@ -60,5 +77,20 @@ public static TValue GetConstantValue<TValue>(this Expression expression, Expres
6077
var message = $"Expression must be a constant: {expression} in {containingExpression}.";
6178
throw new ExpressionNotSupportedException(message);
6279
}
80+
81+
public static bool IsNonDeterministic(this Expression expression)
82+
{
83+
if (expression is MemberExpression memberExpression &&
84+
memberExpression.Member is PropertyInfo propertyInfo)
85+
{
86+
return propertyInfo.IsOneOf(__nonDeterministicProperties);
87+
}
88+
else if (expression is MethodCallExpression methodCallExpression)
89+
{
90+
return methodCallExpression.Method.IsOneOf(__nonDeterministicMethods);
91+
}
92+
93+
return false;
94+
}
6395
}
6496
}

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
using System.Linq;
1919
using System.Linq.Expressions;
2020
using System.Reflection;
21+
using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods;
2122
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
2223

2324
namespace MongoDB.Driver.Linq.Linq3Implementation.Misc
@@ -207,6 +208,18 @@ protected override Expression VisitListInit(ListInitExpression node)
207208
return node;
208209
}
209210

211+
protected override Expression VisitMember(MemberExpression node)
212+
{
213+
var result = base.VisitMember(node);
214+
215+
if (result.IsNonDeterministic())
216+
{
217+
_cannotBeEvaluated = true;
218+
}
219+
220+
return result;
221+
}
222+
210223
protected override Expression VisitMemberInit(MemberInitExpression node)
211224
{
212225
// Bindings must be visited before NewExpression
@@ -231,7 +244,8 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
231244

232245
var method = node.Method;
233246
if (IsCustomLinqExtensionMethod(method) ||
234-
method.Is(QueryableMethod.AsQueryable))
247+
method.Is(QueryableMethod.AsQueryable) ||
248+
result.IsNonDeterministic())
235249
{
236250
_cannotBeEvaluated = true;
237251
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.Reflection;
18+
19+
namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection
20+
{
21+
internal static class DateTimeOffsetProperty
22+
{
23+
// private static fields
24+
private static readonly PropertyInfo __now;
25+
private static readonly PropertyInfo __utcNow;
26+
27+
// static constructor
28+
static DateTimeOffsetProperty()
29+
{
30+
__now = ReflectionInfo.Property(() => DateTimeOffset.Now);
31+
__utcNow = ReflectionInfo.Property(() => DateTimeOffset.UtcNow);
32+
}
33+
34+
// public properties
35+
public static PropertyInfo Now => __now;
36+
public static PropertyInfo UtcNow => __utcNow;
37+
}
38+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.Reflection;
18+
19+
namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection
20+
{
21+
internal static class DateTimeProperty
22+
{
23+
// private static fields
24+
private static readonly PropertyInfo __now;
25+
private static readonly PropertyInfo __today;
26+
private static readonly PropertyInfo __utcNow;
27+
28+
// static constructor
29+
static DateTimeProperty()
30+
{
31+
__now = ReflectionInfo.Property(() => DateTime.Now);
32+
__today = ReflectionInfo.Property(() => DateTime.Today);
33+
__utcNow = ReflectionInfo.Property(() => DateTime.UtcNow);
34+
}
35+
36+
// public properties
37+
public static PropertyInfo Now => __now;
38+
public static PropertyInfo Today => __today;
39+
public static PropertyInfo UtcNow => __utcNow;
40+
}
41+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.Reflection;
18+
19+
namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection
20+
{
21+
internal static class GuidMethod
22+
{
23+
// private static fields
24+
private static readonly MethodInfo __newGuid;
25+
26+
// static constructor
27+
static GuidMethod()
28+
{
29+
__newGuid = ReflectionInfo.Method(() => Guid.NewGuid());
30+
}
31+
32+
// public properties
33+
public static MethodInfo NewGuid => __newGuid;
34+
}
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.Reflection;
18+
19+
namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection
20+
{
21+
internal static class RandomMethod
22+
{
23+
// private static fields
24+
private static readonly MethodInfo __next;
25+
26+
// static constructor
27+
static RandomMethod()
28+
{
29+
__next = ReflectionInfo.Method((Random random) => random.Next());
30+
}
31+
32+
// public properties
33+
public static MethodInfo Next => __next;
34+
}
35+
}

src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/ReflectionInfo.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public static ConstructorInfo Constructor<T1, T2, T3, T4, T5, T6, T7, TObject>(E
6262
return ExtractConstructorInfoFromLambda(lambda);
6363
}
6464

65+
public static MethodInfo Method<TResult>(Expression<Func<TResult>> lambda)
66+
{
67+
return ExtractMethodInfoFromLambda(lambda);
68+
}
69+
6570
public static MethodInfo Method<T1, TResult>(Expression<Func<T1, TResult>> lambda)
6671
{
6772
return ExtractMethodInfoFromLambda(lambda);
@@ -102,6 +107,11 @@ public static MethodInfo Method<T1, T2, T3, T4, T5, T6, T7, T8, TResult>(Express
102107
return ExtractMethodInfoFromLambda(lambda);
103108
}
104109

110+
public static PropertyInfo Property<TProperty>(Expression<Func<TProperty>> lambda)
111+
{
112+
return ExtractPropertyInfoFromLambda(lambda);
113+
}
114+
105115
public static PropertyInfo Property<TObject, TProperty>(Expression<Func<TObject, TProperty>> lambda)
106116
{
107117
return ExtractPropertyInfoFromLambda(lambda);

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using MongoDB.Bson.Serialization.Options;
2424
using MongoDB.Bson.Serialization.Serializers;
2525
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
26+
using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods;
2627
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
2728
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
2829
using MongoDB.Driver.Linq.Linq3Implementation.Serializers;
@@ -36,6 +37,20 @@ public static TranslatedExpression Translate(TranslationContext context, MemberE
3637
{
3738
var containerExpression = expression.Expression;
3839
var member = expression.Member;
40+
var declaringType = expression.Member.DeclaringType;
41+
var memberName = expression.Member.Name;
42+
43+
if (containerExpression == null)
44+
{
45+
// TODO: add support for static properties here
46+
47+
if (expression.IsNonDeterministic())
48+
{
49+
throw new ExpressionNotSupportedException(expression, because: $"non-deterministic field or property '{declaringType.Name}.{memberName}' should not be evaluated client-side and is not currently supported server-side");
50+
}
51+
52+
throw new ExpressionNotSupportedException(expression);
53+
}
3954

4055
if (member is PropertyInfo property && property.DeclaringType.IsNullable())
4156
{

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
using System.Linq.Expressions;
17+
using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods;
1718
using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators;
1819

1920
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators
@@ -22,7 +23,10 @@ internal static class MethodCallExpressionToAggregationExpressionTranslator
2223
{
2324
public static TranslatedExpression Translate(TranslationContext context, MethodCallExpression expression)
2425
{
25-
switch (expression.Method.Name)
26+
var method = expression.Method;
27+
var declaringType = method.DeclaringType;
28+
29+
switch (method.Name)
2630
{
2731
case "Abs": return AbsMethodToAggregationExpressionTranslator.Translate(context, expression);
2832
case "Add": return AddMethodToAggregationExpressionTranslator.Translate(context, expression);
@@ -209,6 +213,11 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC
209213
return TrimMethodToAggregationExpressionTranslator.Translate(context, expression);
210214
}
211215

216+
if (expression.IsNonDeterministic())
217+
{
218+
throw new ExpressionNotSupportedException(expression, because: $"non-deterministic method '{declaringType.Name}.{method.Name}' should not be evaluated client-side and is not currently supported server-side");
219+
}
220+
212221
throw new ExpressionNotSupportedException(expression);
213222
}
214223
}

0 commit comments

Comments
 (0)