From 8dac7264bf0f63875ffa51f06100133d32bb2e5d Mon Sep 17 00:00:00 2001 From: rstam Date: Mon, 10 Nov 2025 15:18:26 -0500 Subject: [PATCH] CSHARP-5691: Non-deterministic functions should not be evaluated client-side --- .../ExtensionMethods/ExpressionExtensions.cs | 32 +++++ .../Misc/PartialEvaluator.cs | 16 ++- .../Reflection/DateTimeOffsetProperty.cs | 38 +++++ .../Reflection/DateTimeProperty.cs | 41 ++++++ .../Reflection/GuidMethod.cs | 35 +++++ .../Reflection/RandomMethod.cs | 35 +++++ .../Reflection/ReflectionInfo.cs | 10 ++ ...essionToAggregationExpressionTranslator.cs | 15 ++ ...essionToAggregationExpressionTranslator.cs | 11 +- .../Jira/CSharp5691Tests.cs | 135 ++++++++++++++++++ 10 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/DateTimeOffsetProperty.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/DateTimeProperty.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/GuidMethod.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/RandomMethod.cs create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5691Tests.cs diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/ExtensionMethods/ExpressionExtensions.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/ExtensionMethods/ExpressionExtensions.cs index db7618ce677..2d505d08306 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/ExtensionMethods/ExpressionExtensions.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/ExtensionMethods/ExpressionExtensions.cs @@ -16,12 +16,29 @@ using System; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using MongoDB.Bson.Serialization; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Reflection; namespace MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods { internal static class ExpressionExtensions { + private readonly static MethodInfo[] __nonDeterministicMethods = + [ + GuidMethod.NewGuid, + RandomMethod.Next + ]; + + private readonly static PropertyInfo[] __nonDeterministicProperties = + [ + DateTimeProperty.Now, + DateTimeProperty.Today, + DateTimeProperty.UtcNow, + DateTimeOffsetProperty.Now, + DateTimeOffsetProperty.UtcNow + ]; public static object Evaluate(this Expression expression) { if (expression is ConstantExpression constantExpression) @@ -60,5 +77,20 @@ public static TValue GetConstantValue(this Expression expression, Expres var message = $"Expression must be a constant: {expression} in {containingExpression}."; throw new ExpressionNotSupportedException(message); } + + public static bool IsNonDeterministic(this Expression expression) + { + if (expression is MemberExpression memberExpression && + memberExpression.Member is PropertyInfo propertyInfo) + { + return propertyInfo.IsOneOf(__nonDeterministicProperties); + } + else if (expression is MethodCallExpression methodCallExpression) + { + return methodCallExpression.Method.IsOneOf(__nonDeterministicMethods); + } + + return false; + } } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/PartialEvaluator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/PartialEvaluator.cs index 171bdf58cc8..bec98035e1d 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/PartialEvaluator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/PartialEvaluator.cs @@ -18,6 +18,7 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; +using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods; using MongoDB.Driver.Linq.Linq3Implementation.Reflection; namespace MongoDB.Driver.Linq.Linq3Implementation.Misc @@ -207,6 +208,18 @@ protected override Expression VisitListInit(ListInitExpression node) return node; } + protected override Expression VisitMember(MemberExpression node) + { + var result = base.VisitMember(node); + + if (result.IsNonDeterministic()) + { + _cannotBeEvaluated = true; + } + + return result; + } + protected override Expression VisitMemberInit(MemberInitExpression node) { // Bindings must be visited before NewExpression @@ -231,7 +244,8 @@ protected override Expression VisitMethodCall(MethodCallExpression node) var method = node.Method; if (IsCustomLinqExtensionMethod(method) || - method.Is(QueryableMethod.AsQueryable)) + method.Is(QueryableMethod.AsQueryable) || + result.IsNonDeterministic()) { _cannotBeEvaluated = true; } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/DateTimeOffsetProperty.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/DateTimeOffsetProperty.cs new file mode 100644 index 00000000000..c95e869da5f --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/DateTimeOffsetProperty.cs @@ -0,0 +1,38 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Reflection; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection +{ + internal static class DateTimeOffsetProperty + { + // private static fields + private static readonly PropertyInfo __now; + private static readonly PropertyInfo __utcNow; + + // static constructor + static DateTimeOffsetProperty() + { + __now = ReflectionInfo.Property(() => DateTimeOffset.Now); + __utcNow = ReflectionInfo.Property(() => DateTimeOffset.UtcNow); + } + + // public properties + public static PropertyInfo Now => __now; + public static PropertyInfo UtcNow => __utcNow; + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/DateTimeProperty.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/DateTimeProperty.cs new file mode 100644 index 00000000000..bb1bf37c101 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/DateTimeProperty.cs @@ -0,0 +1,41 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Reflection; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection +{ + internal static class DateTimeProperty + { + // private static fields + private static readonly PropertyInfo __now; + private static readonly PropertyInfo __today; + private static readonly PropertyInfo __utcNow; + + // static constructor + static DateTimeProperty() + { + __now = ReflectionInfo.Property(() => DateTime.Now); + __today = ReflectionInfo.Property(() => DateTime.Today); + __utcNow = ReflectionInfo.Property(() => DateTime.UtcNow); + } + + // public properties + public static PropertyInfo Now => __now; + public static PropertyInfo Today => __today; + public static PropertyInfo UtcNow => __utcNow; + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/GuidMethod.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/GuidMethod.cs new file mode 100644 index 00000000000..18f3766feb3 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/GuidMethod.cs @@ -0,0 +1,35 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Reflection; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection +{ + internal static class GuidMethod + { + // private static fields + private static readonly MethodInfo __newGuid; + + // static constructor + static GuidMethod() + { + __newGuid = ReflectionInfo.Method(() => Guid.NewGuid()); + } + + // public properties + public static MethodInfo NewGuid => __newGuid; + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/RandomMethod.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/RandomMethod.cs new file mode 100644 index 00000000000..33435f93cab --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/RandomMethod.cs @@ -0,0 +1,35 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Reflection; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection +{ + internal static class RandomMethod + { + // private static fields + private static readonly MethodInfo __next; + + // static constructor + static RandomMethod() + { + __next = ReflectionInfo.Method((Random random) => random.Next()); + } + + // public properties + public static MethodInfo Next => __next; + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/ReflectionInfo.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/ReflectionInfo.cs index 9cc03bdf517..8ccc5c08c44 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/ReflectionInfo.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/ReflectionInfo.cs @@ -62,6 +62,11 @@ public static ConstructorInfo Constructor(E return ExtractConstructorInfoFromLambda(lambda); } + public static MethodInfo Method(Expression> lambda) + { + return ExtractMethodInfoFromLambda(lambda); + } + public static MethodInfo Method(Expression> lambda) { return ExtractMethodInfoFromLambda(lambda); @@ -102,6 +107,11 @@ public static MethodInfo Method(Express return ExtractMethodInfoFromLambda(lambda); } + public static PropertyInfo Property(Expression> lambda) + { + return ExtractPropertyInfoFromLambda(lambda); + } + public static PropertyInfo Property(Expression> lambda) { return ExtractPropertyInfoFromLambda(lambda); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs index 58cf7f6ab61..13b058d3840 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs @@ -23,6 +23,7 @@ using MongoDB.Bson.Serialization.Options; using MongoDB.Bson.Serialization.Serializers; using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods; using MongoDB.Driver.Linq.Linq3Implementation.Misc; using MongoDB.Driver.Linq.Linq3Implementation.Reflection; using MongoDB.Driver.Linq.Linq3Implementation.Serializers; @@ -36,6 +37,20 @@ public static TranslatedExpression Translate(TranslationContext context, MemberE { var containerExpression = expression.Expression; var member = expression.Member; + var declaringType = expression.Member.DeclaringType; + var memberName = expression.Member.Name; + + if (containerExpression == null) + { + // TODO: add support for static properties here + + if (expression.IsNonDeterministic()) + { + 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"); + } + + throw new ExpressionNotSupportedException(expression); + } if (member is PropertyInfo property && property.DeclaringType.IsNullable()) { diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs index 4dbbe32fdd7..47cf291aeca 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs @@ -14,6 +14,7 @@ */ using System.Linq.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods; using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators; namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators @@ -22,7 +23,10 @@ internal static class MethodCallExpressionToAggregationExpressionTranslator { public static TranslatedExpression Translate(TranslationContext context, MethodCallExpression expression) { - switch (expression.Method.Name) + var method = expression.Method; + var declaringType = method.DeclaringType; + + switch (method.Name) { case "Abs": return AbsMethodToAggregationExpressionTranslator.Translate(context, expression); case "Add": return AddMethodToAggregationExpressionTranslator.Translate(context, expression); @@ -209,6 +213,11 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC return TrimMethodToAggregationExpressionTranslator.Translate(context, expression); } + if (expression.IsNonDeterministic()) + { + 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"); + } + throw new ExpressionNotSupportedException(expression); } } diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5691Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5691Tests.cs new file mode 100644 index 00000000000..c7db5855769 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5691Tests.cs @@ -0,0 +1,135 @@ +/* Copyright 2010-present MongoDB Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using MongoDB.Driver.TestHelpers; +using FluentAssertions; +using MongoDB.Driver.Linq; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira; + +public class CSharp5691Tests : LinqIntegrationTest +{ + public CSharp5691Tests(ClassFixture fixture) + : base(fixture) + { + } + + [Fact] + public void DateTime_Now_should_not_be_evaluated_client_side() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => DateTime.Now); + + var exception = Record.Exception(() => Translate(collection, queryable)); + exception.Should().BeOfType(); + exception.Message.Should().Contain("non-deterministic field or property 'DateTime.Now' should not be evaluated client-side and is not currently supported server-side"); + } + + [Fact] + public void DateTime_UtcNow_should_not_be_evaluated_client_side() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => DateTime.UtcNow); + + var exception = Record.Exception(() => Translate(collection, queryable)); + exception.Should().BeOfType(); + exception.Message.Should().Contain("non-deterministic field or property 'DateTime.UtcNow' should not be evaluated client-side and is not currently supported server-side"); + } + + [Fact] + public void DateTime_Today_should_not_be_evaluated_client_side() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => DateTime.Today); + + var exception = Record.Exception(() => Translate(collection, queryable)); + exception.Should().BeOfType(); + exception.Message.Should().Contain("non-deterministic field or property 'DateTime.Today' should not be evaluated client-side and is not currently supported server-side"); + } + + [Fact] + public void DateTimeOffset_Now_should_not_be_evaluated_client_side() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => DateTimeOffset.Now); + + var exception = Record.Exception(() => Translate(collection, queryable)); + exception.Should().BeOfType(); + exception.Message.Should().Contain("non-deterministic field or property 'DateTimeOffset.Now' should not be evaluated client-side and is not currently supported server-side"); + } + + [Fact] + public void DateTimeOffset_UtcNow_should_not_be_evaluated_client_side() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => DateTimeOffset.UtcNow); + + var exception = Record.Exception(() => Translate(collection, queryable)); + exception.Should().BeOfType(); + exception.Message.Should().Contain("non-deterministic field or property 'DateTimeOffset.UtcNow' should not be evaluated client-side and is not currently supported server-side"); + } + + [Fact] + public void Guid_NewGuid_should_not_be_evaluated_client_side() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => Guid.NewGuid()); + + var exception = Record.Exception(() => Translate(collection, queryable)); + exception.Should().BeOfType(); + exception.Message.Should().Contain("non-deterministic method 'Guid.NewGuid' should not be evaluated client-side and is not currently supported server-side"); + } + + [Fact] + public void Random_Next_should_not_be_evaluated_client_side() + { + var collection = Fixture.Collection; + + var queryable = collection.AsQueryable() + .Select(x => new Random().Next()); + + var exception = Record.Exception(() => Translate(collection, queryable)); + exception.Message.Should().Contain("non-deterministic method 'Random.Next' should not be evaluated client-side and is not currently supported server-side"); + } + + public class C + { + public int Id { get; set; } + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData => + [ + new C { Id = 1 } + ]; + } +}