From 34ca6d3a683c0a15b9f444b70642b26107c78df8 Mon Sep 17 00:00:00 2001 From: rstam Date: Wed, 8 Jan 2025 14:02:32 -0800 Subject: [PATCH] CSHARP-5321: Optimize client-side projections to perform as much as possible of the projection on the server. --- ...lientSideProjectionSnippetsDeserializer.cs | 82 ++++++++++++ .../Misc/ExpressionIsReferencedVisitor.cs | 9 ++ .../Misc/LambdaExpressionExtensions.cs | 4 +- .../ClientSideProjectionExpressionRewriter.cs | 103 +++++++++++++++ .../ClientSideProjectionSnippetsTranslator.cs | 120 ++++++++++++++++++ .../SelectMethodToPipelineTranslator.cs | 13 +- .../Linq/LinqProviderAdapter.cs | 6 +- .../Jira/CSharp3529Tests.cs | 12 +- .../Jira/CSharp3958Tests.cs | 44 +++---- .../Jira/CSharp4100ExpressionTests.cs | 50 +++++--- .../Jira/CSharp4410Tests.cs | 20 ++- .../Jira/CSharp4486Tests.cs | 3 +- .../Jira/CSharp4517Tests.cs | 5 +- .../Jira/CSharp4763Tests.cs | 100 ++++++++++++--- .../Jira/CSharp4813Tests.cs | 11 +- .../Jira/CSharp4957Tests.cs | 5 +- .../Jira/CSharp5043Tests.cs | 8 +- .../Jira/CSharp5321Tests.cs | 101 +++++++++++++++ ...nToAggregationExpressionTranslatorTests.cs | 5 +- 19 files changed, 611 insertions(+), 90 deletions(-) create mode 100644 src/MongoDB.Driver/ClientSideProjectionSnippetsDeserializer.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ClientSideProjectionExpressionRewriter.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ClientSideProjectionSnippetsTranslator.cs create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5321Tests.cs diff --git a/src/MongoDB.Driver/ClientSideProjectionSnippetsDeserializer.cs b/src/MongoDB.Driver/ClientSideProjectionSnippetsDeserializer.cs new file mode 100644 index 00000000000..195fa3ac2cf --- /dev/null +++ b/src/MongoDB.Driver/ClientSideProjectionSnippetsDeserializer.cs @@ -0,0 +1,82 @@ +/* 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.Linq; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace MongoDB.Driver +{ + internal static class ClientSideProjectionSnippetsDeserializer + { + public static IBsonSerializer Create( + Type projectionType, + IBsonSerializer[] snippetDeserializers, + Delegate projector) + { + var deserializerType = typeof(ClientSideProjectionSnippetsDeserializer<>).MakeGenericType(projectionType); + return (IBsonSerializer)Activator.CreateInstance(deserializerType, [snippetDeserializers, projector]); + } + } + + internal sealed class ClientSideProjectionSnippetsDeserializer : SerializerBase, IClientSideProjectionDeserializer + { + private readonly IBsonSerializer[] _snippetDeserializers; + private readonly Func _projector; + + public ClientSideProjectionSnippetsDeserializer(IBsonSerializer[] snippetDeserializers, Func projector) + { + _snippetDeserializers = snippetDeserializers; + _projector = projector; + } + + public override TProjection Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var snippets = DeserializeSnippets(context); + return _projector(snippets); + } + + private object[] DeserializeSnippets(BsonDeserializationContext context) + { + var reader = context.Reader; + + reader.ReadStartDocument(); + reader.ReadName("_snippets"); + reader.ReadStartArray(); + var snippets = new object[_snippetDeserializers.Length]; + var i = 0; + while (reader.ReadBsonType() != BsonType.EndOfDocument) + { + if (i >= _snippetDeserializers.Length) + { + throw new BsonSerializationException($"Expected {_snippetDeserializers.Length} snippets but found more than that."); + } + snippets[i] = _snippetDeserializers[i].Deserialize(context); + i++; + } + if (i != _snippetDeserializers.Length) + { + throw new BsonSerializationException($"Expected {_snippetDeserializers.Length} snippets but found {i}."); + } + reader.ReadEndArray(); + reader.ReadEndDocument(); + + return snippets; + } + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ExpressionIsReferencedVisitor.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ExpressionIsReferencedVisitor.cs index c5b460d6b07..444915c5aaa 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ExpressionIsReferencedVisitor.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ExpressionIsReferencedVisitor.cs @@ -20,6 +20,15 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Misc { internal class ExpressionIsReferencedVisitor : ExpressionVisitor { + #region static + public static bool IsReferenced(Expression node, Expression expression) + { + var visitor = new ExpressionIsReferencedVisitor(expression); + visitor.Visit(node); + return visitor.ExpressionIsReferenced; + } + #endregion + private readonly Expression _expression; private bool _expressionIsReferenced; diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/LambdaExpressionExtensions.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/LambdaExpressionExtensions.cs index e0fb4d9efbb..5f99ba71cf0 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/LambdaExpressionExtensions.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/LambdaExpressionExtensions.cs @@ -26,9 +26,7 @@ internal static class LambdaExpressionExtensions { public static bool LambdaBodyReferencesParameter(this LambdaExpression lambda, ParameterExpression parameter) { - var visitor = new ExpressionIsReferencedVisitor(parameter); - visitor.Visit(lambda.Body); - return visitor.ExpressionIsReferenced; + return ExpressionIsReferencedVisitor.IsReferenced(lambda.Body, parameter); } public static string TranslateToDottedFieldName(this LambdaExpression fieldSelectorLambda, TranslationContext context, IBsonSerializer parameterSerializer) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ClientSideProjectionExpressionRewriter.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ClientSideProjectionExpressionRewriter.cs new file mode 100644 index 00000000000..3f73ed99a44 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ClientSideProjectionExpressionRewriter.cs @@ -0,0 +1,103 @@ +/* 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.Linq; +using System.Linq.Expressions; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Stages; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators +{ + internal static class ClientSideProjectionExpressionRewriter + { + public static (AstProjectStage, IBsonSerializer) CreateClientSideProjection( + TranslationContext context, + LambdaExpression projectionLambda, + IBsonSerializer sourceSerializer) + { + var (snippetsExpression, snippetsProjectionDeserializer) = ClientSideProjectionExpressionRewriter.TranslateLambdaBodyUsingSnippets(context, sourceSerializer, projectionLambda); + if (snippetsExpression == null) + { + return (null, snippetsProjectionDeserializer); + } + else + { + var snippetsTranslation = new AggregationExpression(projectionLambda, snippetsExpression, snippetsProjectionDeserializer); + return ProjectionHelper.CreateProjectStage(snippetsTranslation); + } + } + + private static (AstComputedDocumentExpression, IBsonSerializer) TranslateLambdaBodyUsingSnippets( + TranslationContext context, + IBsonSerializer sourceSerializer, + LambdaExpression projectionLambda) + { + var wireVersion = context.TranslationOptions.CompatibilityLevel.ToWireVersion(); + if (!Feature.FindProjectionExpressions.IsSupported(wireVersion)) + { + var clientSideProjectionDeserializer = ClientSideProjectionDeserializer.Create(sourceSerializer, projectionLambda); + return (null, clientSideProjectionDeserializer); // project directly off $$ROOT with no snippets + } + + var snippets = ClientSideProjectionSnippetsTranslator.TranslateSnippets(context, projectionLambda, sourceSerializer); + + if (snippets.Length == 0 || snippets.Any(IsRoot)) + { + var clientSideProjectionDeserializer = ClientSideProjectionDeserializer.Create(sourceSerializer, projectionLambda); + return (null, clientSideProjectionDeserializer); // project directly off $$ROOT with no snippets + } + else + { + var snippetsComputedDocument = CreateSnippetsComputedDocument(snippets); + var snippetDeserializers = snippets.Select(s => s.Serializer).ToArray(); + var rewrittenSelectorLamdba = RewriteSelector(projectionLambda, snippets); + var rewrittenSelectorDelegate = rewrittenSelectorLamdba.Compile(); + var clientSideProjectionSnippetsDeserializer = ClientSideProjectionSnippetsDeserializer.Create(projectionLambda.ReturnType, snippetDeserializers, rewrittenSelectorDelegate); + return (snippetsComputedDocument, clientSideProjectionSnippetsDeserializer); + } + + static bool IsRoot(AggregationExpression snippet) => snippet.Ast.IsRootVar(); + } + + private static AstComputedDocumentExpression CreateSnippetsComputedDocument(AggregationExpression[] snippets) + { + var snippetsArray = AstExpression.ComputedArray(snippets.Select(s => s.Ast)); + var snippetsdField = AstExpression.ComputedField("_snippets", snippetsArray); + return (AstComputedDocumentExpression)AstExpression.ComputedDocument([snippetsdField]); + + } + + private static LambdaExpression RewriteSelector(LambdaExpression selectorLambda, AggregationExpression[] snippets) + { + var rewrittenBody = selectorLambda.Body; + var snippetsParameter = Expression.Parameter(typeof(object[]), "snippets"); + + for (var i = 0; i < snippets.Length; i++) + { + var snippet = snippets[i]; + var snippetReference = // (T)_snippets[i] + Expression.Convert( + Expression.ArrayIndex(snippetsParameter, Expression.Constant(i)), + snippet.Expression.Type); + rewrittenBody = ExpressionReplacer.Replace(rewrittenBody, snippet.Expression, snippetReference); + } + + return Expression.Lambda(rewrittenBody, snippetsParameter); + } + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ClientSideProjectionSnippetsTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ClientSideProjectionSnippetsTranslator.cs new file mode 100644 index 00000000000..ff74a684324 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/ClientSideProjectionSnippetsTranslator.cs @@ -0,0 +1,120 @@ +/* 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 System.Linq.Expressions; +using System.Reflection; +using MongoDB.Bson.Serialization; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Reflection; +using ExpressionVisitor = System.Linq.Expressions.ExpressionVisitor; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators +{ + internal class ClientSideProjectionSnippetsTranslator : ExpressionVisitor + { + #region static + + private readonly static MethodInfo[] __orderByMethods = + [ + EnumerableMethod.OrderBy, + EnumerableMethod.OrderByDescending, + QueryableMethod.OrderBy, + QueryableMethod.OrderByDescending + ]; + + private readonly static MethodInfo[] __thenByMethods = + [ + EnumerableMethod.ThenBy, + EnumerableMethod.ThenByDescending, + QueryableMethod.ThenBy, + QueryableMethod.ThenByDescending + ]; + + public static AggregationExpression[] TranslateSnippets(TranslationContext context, LambdaExpression selectorLambda, IBsonSerializer sourceSerializer) + { + var rootParameter = selectorLambda.Parameters.Single(); + var rootSymbol = context.CreateRootSymbol(rootParameter, sourceSerializer); + context = context.WithSymbol(rootSymbol); + + var snippetTranslator = new ClientSideProjectionSnippetsTranslator(context, rootParameter); + snippetTranslator.Visit(selectorLambda.Body); + + return snippetTranslator.Snippets.ToArray(); + } + + #endregion + + private readonly TranslationContext _context; + private readonly ParameterExpression _rootParameter; + private readonly List _snippets = new(); + + private ClientSideProjectionSnippetsTranslator(TranslationContext context, ParameterExpression rootParameter) + { + _context = context; + _rootParameter = rootParameter; + } + + private List Snippets => _snippets; + + public override Expression Visit(Expression node) + { + if (ExpressionIsReferencedVisitor.IsReferenced(node, _rootParameter)) + { + try + { + var snippet = ExpressionToAggregationExpressionTranslator.Translate(_context, node); + _snippets.Add(snippet); + return node; + } + catch + { + // don't split OrderBy/ThenBy between client and server + if (node is MethodCallExpression methodCallExpression && + methodCallExpression.Method.IsOneOf(__thenByMethods)) + { + var orderBySource = FindOrderBySource(node); + Visit(orderBySource); // resume visiting at orderBySource + return node; // suppress any further visiting below this node + } + + // ignore exceptions and fall through + } + } + + return base.Visit(node); + + static Expression FindOrderBySource(Expression node) + { + if (node is MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Method.IsOneOf(__thenByMethods)) + { + return FindOrderBySource(methodCallExpression.Arguments[0]); + } + + if (methodCallExpression.Method.IsOneOf(__orderByMethods)) + { + return methodCallExpression.Arguments[0]; + } + } + + throw new ArgumentException($"Node type {node.NodeType} is not a MethodCallExpression."); + } + } + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/SelectMethodToPipelineTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/SelectMethodToPipelineTranslator.cs index e66b2907082..6362cba2eef 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/SelectMethodToPipelineTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/SelectMethodToPipelineTranslator.cs @@ -15,6 +15,7 @@ using System; using System.Linq.Expressions; +using MongoDB.Bson.Serialization; using MongoDB.Driver.Linq.Linq3Implementation.Ast; using MongoDB.Driver.Linq.Linq3Implementation.Ast.Stages; using MongoDB.Driver.Linq.Linq3Implementation.Misc; @@ -46,19 +47,21 @@ public static TranslatedPipeline Translate(TranslationContext context, MethodCal ClientSideProjectionHelper.ThrowIfClientSideProjection(expression, pipeline, method); var sourceSerializer = pipeline.OutputSerializer; + AstProjectStage projectStage; + IBsonSerializer projectionSerializer; try { var selectorTranslation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, selectorLambda, sourceSerializer, asRoot: true); - var (projectStage, projectionSerializer) = ProjectionHelper.CreateProjectStage(selectorTranslation); - pipeline = pipeline.AddStages(projectionSerializer, projectStage); + (projectStage, projectionSerializer) = ProjectionHelper.CreateProjectStage(selectorTranslation); } catch (ExpressionNotSupportedException) when (context.TranslationOptions?.EnableClientSideProjections ?? false) { - var clientSideProjectionDeserializer = ClientSideProjectionDeserializer.Create(sourceSerializer, selectorLambda); - pipeline = pipeline.AddStages(clientSideProjectionDeserializer, Array.Empty()); + (projectStage, projectionSerializer) = ClientSideProjectionExpressionRewriter.CreateClientSideProjection(context, selectorLambda, sourceSerializer); } - return pipeline; + return projectStage == null ? + new TranslatedPipeline(pipeline.Ast, projectionSerializer) : // just switch the output serializer + pipeline.AddStages(projectionSerializer, projectStage); } throw new ExpressionNotSupportedException(expression); diff --git a/src/MongoDB.Driver/Linq/LinqProviderAdapter.cs b/src/MongoDB.Driver/Linq/LinqProviderAdapter.cs index d0c1eb8a21b..d7a1924f46c 100644 --- a/src/MongoDB.Driver/Linq/LinqProviderAdapter.cs +++ b/src/MongoDB.Driver/Linq/LinqProviderAdapter.cs @@ -188,9 +188,9 @@ private static RenderedProjectionDefinition TranslateExpressionToProjec } catch (ExpressionNotSupportedException) when (translationOptions?.EnableClientSideProjections ?? false) { - var projectorDelegate = expression.Compile(); - var clientSideProjectionDeserializer = new ClientSideProjectionDeserializer(inputSerializer, projectorDelegate); - return new RenderedProjectionDefinition(document: null, clientSideProjectionDeserializer); + var (projectStage, projectionSerializer) = ClientSideProjectionExpressionRewriter.CreateClientSideProjection(context, expression, inputSerializer); + var projectionDocument = projectStage == null ? null : AstSimplifier.SimplifyAndConvert(projectStage).Render()["$project"].AsBsonDocument; + return new RenderedProjectionDefinition(projectionDocument, (IBsonSerializer)projectionSerializer); } } diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp3529Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp3529Tests.cs index a1ac556283b..28894ef02b6 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp3529Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp3529Tests.cs @@ -165,9 +165,9 @@ public void Bottom_without_GroupBy_should_have_helpful_error_message( if (enableClientSideProjections) { - var stages = Translate(collection, queryable, out var serializer); - AssertStages(stages, Array.Empty()); - serializer.Should().BeOfType>(); + var stages = Translate(collection, queryable, out var outputSerializer); + AssertStages(stages, "{ $project : { _snippets : ['$A'], _id : 0 } }"); + outputSerializer.Should().BeAssignableTo(); var exception = Record.Exception(() => queryable.ToList()); exception.Should().BeOfType(); @@ -398,7 +398,7 @@ public void BottomN_without_GroupBy_should_have_helpful_error_message( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$A'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var exception = Record.Exception(() => queryable.ToList()); @@ -1660,7 +1660,7 @@ public void Top_without_GroupBy_should_have_helpful_error_message( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$A'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var exception = Record.Exception(() => queryable.ToList()); @@ -1892,7 +1892,7 @@ public void TopN_without_GroupBy_should_have_helpful_error_message( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$A'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var exception = Record.Exception(() => queryable.ToList()); diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp3958Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp3958Tests.cs index 71cf29ec987..b629728d726 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp3958Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp3958Tests.cs @@ -73,9 +73,9 @@ public void Sort_on_multiple_fields_example_should_work() var stages = Translate(collection, queryable); AssertStages(stages, "{ $project : { Result : { $sortArray : { input : '$Team', sortBy : { 'Age' : -1, Name : 1 } } }, _id : 0 } }"); - var results = queryable.ToList(); - results.Should().HaveCount(1); - results[0].Result.Select(m => m.Age).Should().Equal(42, 36, 30); + var result = queryable.Single(); + result.Result.Select(m => m.Age).Should().Equal(42, 30, 30); + result.Result.Select(m => m.Name).Should().Equal("Pat", "Charlie", "Dallas"); } [Fact] @@ -156,17 +156,12 @@ public void OrderBy_on_entire_object_followed_by_ThenBy_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$Team'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var result = queryable.Single(); - var exception = Record.Exception(() => result.Result.ToList()); -#if NET472_OR_GREATER - // .NET Framework throws a different exception than other target frameworks - exception.Should().BeOfType(); // TeamMembers are not IComparable -#else - exception.Should().BeOfType(); // TeamMembers are not IComparable -#endif + result.Result.Select(m => m.Age).Should().Equal(30, 30, 42); + result.Result.Select(m => m.Name).Should().Equal("Charlie", "Dallas", "Pat"); } else { @@ -192,17 +187,12 @@ public void OrderByDescending_on_entire_object_followed_by_ThenBy_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$Team'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var result = queryable.Single(); - var exception = Record.Exception(() => result.Result.ToList()); -#if NET472_OR_GREATER - // .NET Framework throws a different exception than other target frameworks - exception.Should().BeOfType(); // TeamMembers are not IComparable -#else - exception.Should().BeOfType(); // TeamMembers are not IComparable -#endif + result.Result.Select(m => m.Age).Should().Equal(30, 30, 42); + result.Result.Select(m => m.Name).Should().Equal("Charlie", "Dallas", "Pat"); } else { @@ -228,7 +218,7 @@ public void ThenBy_on_entire_object_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$Team'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var result = queryable.Single(); @@ -258,7 +248,7 @@ public void ThenByDescending_on_entire_object_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$Team'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var result = queryable.Single(); @@ -318,7 +308,7 @@ public void IOrderedEnumerableSerializer_Serialize_should_work() var result = queryable.Single(); var json = result.ToJson(); - json.Should().Be("{ \"Result\" : [{ \"Name\" : \"Charlie\", \"Age\" : 42, \"Address\" : { \"Street\" : \"12 French St\", \"City\" : \"New Brunswick\" } }, { \"Name\" : \"Dallas\", \"Age\" : 36, \"Address\" : { \"Street\" : \"12 Cowper St\", \"City\" : \"Palo Alto\" } }, { \"Name\" : \"Pat\", \"Age\" : 30, \"Address\" : { \"Street\" : \"12 Baker St\", \"City\" : \"London\" } }] }"); + json.Should().Be("{ \"Result\" : [{ \"Name\" : \"Charlie\", \"Age\" : 30, \"Address\" : { \"Street\" : \"12 French St\", \"City\" : \"New Brunswick\" } }, { \"Name\" : \"Dallas\", \"Age\" : 30, \"Address\" : { \"Street\" : \"12 Cowper St\", \"City\" : \"Palo Alto\" } }, { \"Name\" : \"Pat\", \"Age\" : 42, \"Address\" : { \"Street\" : \"12 Baker St\", \"City\" : \"London\" } }] }"); } private IMongoCollection CreateEngineersCollection() @@ -332,9 +322,9 @@ private IMongoCollection CreateEngineersCollection() Id = 1, Team = new[] { - new TeamMember { Name = "Pat", Age = 30, Address = new Address { Street = "12 Baker St", City = "London"}}, - new TeamMember { Name = "Dallas", Age = 36, Address = new Address { Street = "12 Cowper St", City = "Palo Alto"}}, - new TeamMember { Name = "Charlie", Age = 42, Address = new Address { Street = "12 French St", City = "New Brunswick"}} + new TeamMember { Name = "Pat", Age = 42, Address = new Address { Street = "12 Baker St", City = "London"}}, + new TeamMember { Name = "Dallas", Age = 30, Address = new Address { Street = "12 Cowper St", City = "Palo Alto"}}, + new TeamMember { Name = "Charlie", Age = 30, Address = new Address { Street = "12 French St", City = "New Brunswick"}} } }); @@ -347,11 +337,13 @@ private class Engineers public TeamMember[] Team { get; set; } } - private class TeamMember + private class TeamMember : IComparable { public string Name { get; set; } public int Age { get; set; } public Address Address { get; set; } + + public int CompareTo(TeamMember other) => Age.CompareTo(other.Age); } private class Address diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4100ExpressionTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4100ExpressionTests.cs index f0babc16c63..db534655441 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4100ExpressionTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4100ExpressionTests.cs @@ -19,6 +19,8 @@ using FluentAssertions; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; using MongoDB.TestHelpers.XunitExtensions; using Xunit; @@ -80,6 +82,7 @@ public void Contains_with_string_constant_and_char_field_represented_as_string_s public void Contains_with_string_field_and_char_field_not_represented_as_string_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -89,7 +92,7 @@ public void Contains_with_string_field_and_char_field_not_represented_as_string_ if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var serializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S', '$CC'], _id : 0 } }"); serializer.Should().BeAssignableTo(); } else @@ -105,6 +108,7 @@ public void Contains_with_string_field_and_char_field_not_represented_as_string_ public void Contains_with_string_constant_and_char_field_not_represented_as_string_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -114,7 +118,7 @@ public void Contains_with_string_constant_and_char_field_not_represented_as_stri if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var serializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$CC'], _id : 0 } }"); serializer.Should().BeAssignableTo(); } else @@ -198,6 +202,7 @@ public void Contains_with_string_field_and_char_value_not_represented_as_string_ [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -207,7 +212,7 @@ public void Contains_with_string_field_and_char_value_not_represented_as_string_ if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S', '$CC'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -231,6 +236,7 @@ public void Contains_with_string_constant_and_char_value_not_represented_as_stri [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -240,7 +246,7 @@ public void Contains_with_string_constant_and_char_value_not_represented_as_stri if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$CC'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -268,6 +274,7 @@ public void Contains_with_string_field_and_char_value_and_invalid_comparisonType [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -277,7 +284,7 @@ public void Contains_with_string_field_and_char_value_and_invalid_comparisonType if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -435,6 +442,7 @@ public void Contains_with_string_field_and_string_value_and_invalid_comparisonTy [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -444,7 +452,7 @@ public void Contains_with_string_field_and_string_value_and_invalid_comparisonTy if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -539,6 +547,7 @@ public void EndsWith_with_string_constant_and_char_field_represented_as_string_s public void EndsWith_with_string_field_and_char_field_not_represented_as_string_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -548,7 +557,7 @@ public void EndsWith_with_string_field_and_char_field_not_represented_as_string_ if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S', '$CC'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -569,6 +578,7 @@ public void EndsWith_with_string_field_and_char_field_not_represented_as_string_ public void EndsWith_with_string_constant_and_char_field_not_represented_as_string_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -578,7 +588,7 @@ public void EndsWith_with_string_constant_and_char_field_not_represented_as_stri if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$CC'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -703,6 +713,7 @@ public void EndsWith_with_string_field_and_string_value_and_ignoreCase_and_inval [Values(false, true)] bool ignoreCase, [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; var notCurrentCulture = GetACultureThatIsNotTheCurrentCulture(); @@ -713,7 +724,7 @@ public void EndsWith_with_string_field_and_string_value_and_ignoreCase_and_inval if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -809,6 +820,7 @@ public void EndsWith_with_string_field_and_string_value_and_invalid_comparisonTy [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -818,7 +830,7 @@ public void EndsWith_with_string_field_and_string_value_and_invalid_comparisonTy if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S', '$T'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -844,6 +856,7 @@ public void EndsWith_with_string_constant_and_string_value_and_invalid_compariso [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -853,7 +866,7 @@ public void EndsWith_with_string_constant_and_string_value_and_invalid_compariso if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$T'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -929,6 +942,7 @@ public void StartsWith_with_string_constant_and_char_field_represented_as_string public void StartsWith_with_string_field_and_char_field_not_represented_as_string_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -938,7 +952,7 @@ public void StartsWith_with_string_field_and_char_field_not_represented_as_strin if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S', '$CC'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -959,6 +973,7 @@ public void StartsWith_with_string_field_and_char_field_not_represented_as_strin public void StartsWith_with_string_constant_and_char_field_not_represented_as_string_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -968,7 +983,7 @@ public void StartsWith_with_string_constant_and_char_field_not_represented_as_st if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$CC'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -1093,6 +1108,7 @@ public void StartsWith_with_string_field_and_string_value_and_ignoreCase_and_inv [Values(false, true)] bool ignoreCase, [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; var notCurrentCulture = GetACultureThatIsNotTheCurrentCulture(); @@ -1103,7 +1119,7 @@ public void StartsWith_with_string_field_and_string_value_and_ignoreCase_and_inv if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -1198,6 +1214,7 @@ public void StartsWith_with_string_field_and_string_value_and_invalid_comparison StringComparison comparisonType, [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -1207,7 +1224,7 @@ public void StartsWith_with_string_field_and_string_value_and_invalid_comparison if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S', '$T'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -1233,6 +1250,7 @@ public void StartsWith_with_string_constant_and_string_value_and_invalid_compari [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -1242,7 +1260,7 @@ public void StartsWith_with_string_constant_and_string_value_and_invalid_compari if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$T'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4410Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4410Tests.cs index 1cfc5d8f434..3f0e65febf9 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4410Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4410Tests.cs @@ -20,6 +20,8 @@ using FluentAssertions; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; using MongoDB.TestHelpers.XunitExtensions; using Xunit; @@ -180,6 +182,7 @@ public void Select_with_enum_comparison_should_work(int i, string projectionAsSt public void Comparison_of_enum_and_enum_with_mismatched_serializers_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = CreateCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -190,7 +193,7 @@ public void Comparison_of_enum_and_enum_with_mismatched_serializers_should_throw if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$E', '$S'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -209,6 +212,7 @@ public void Comparison_of_enum_and_enum_with_mismatched_serializers_should_throw public void Comparison_of_enum_and_nullable_enum_with_mismatched_serializers_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = CreateCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -219,7 +223,7 @@ public void Comparison_of_enum_and_nullable_enum_with_mismatched_serializers_sho if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$E', '$NS'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -238,6 +242,7 @@ public void Comparison_of_enum_and_nullable_enum_with_mismatched_serializers_sho public void Comparison_of_nullable_enum_and_enum_with_mismatched_serializers_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = CreateCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -248,7 +253,7 @@ public void Comparison_of_nullable_enum_and_enum_with_mismatched_serializers_sho if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$NE', '$S'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -267,6 +272,7 @@ public void Comparison_of_nullable_enum_and_enum_with_mismatched_serializers_sho public void Comparison_of_nullable_enum_and_nullable_enum_with_mismatched_serializers_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = CreateCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -277,7 +283,7 @@ public void Comparison_of_nullable_enum_and_nullable_enum_with_mismatched_serial if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$NE', '$NS'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -296,6 +302,7 @@ public void Comparison_of_nullable_enum_and_nullable_enum_with_mismatched_serial public void Arithmetic_with_enum_represented_as_string_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = CreateCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -306,7 +313,7 @@ public void Arithmetic_with_enum_represented_as_string_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$S'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -325,6 +332,7 @@ public void Arithmetic_with_enum_represented_as_string_should_throw( public void Arithmetic_with_nullable_enum_represented_as_string_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = CreateCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -335,7 +343,7 @@ public void Arithmetic_with_nullable_enum_represented_as_string_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$NS'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4486Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4486Tests.cs index 076482918ce..66ec89f4ff4 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4486Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4486Tests.cs @@ -106,6 +106,7 @@ public void Or_with_three_arguments_should_work() public void Xor_with_two_arguments_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -115,7 +116,7 @@ public void Xor_with_two_arguments_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$P', '$Q'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4517Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4517Tests.cs index cce93e1be38..eb5fb62d23f 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4517Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4517Tests.cs @@ -20,6 +20,8 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; using MongoDB.TestHelpers.XunitExtensions; using Xunit; @@ -48,6 +50,7 @@ public void Filter_with_comparison_of_different_types_should_throw() public void Expression_with_comparison_of_different_types_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = CreateCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -58,7 +61,7 @@ public void Expression_with_comparison_of_different_types_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$_id'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4763Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4763Tests.cs index 0809df2f8cb..b5d7a8aca5f 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4763Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4763Tests.cs @@ -16,6 +16,7 @@ using System; using System.Linq; using FluentAssertions; +using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Linq; using Xunit; @@ -44,7 +45,14 @@ public void Find_with_client_side_projection_ToList_should_work( if (enableClientSideProjections ?? false) { var projection = TranslateFindProjection(collection, find, out var projectionSerializer); - projection.Should().BeNull(); + if (findOptions.TranslationOptions.CompatibilityLevel == ServerVersion.Server42) + { + projection.Should().BeNull(); + } + else + { + projection.Should().Be("{ _snippets : ['$X'], _id : 0 }"); + } projectionSerializer.Should().BeAssignableTo(); var results = find.ToList(); @@ -70,8 +78,7 @@ public void Find_with_client_side_projection_First_should_work( bool? enableClientSideProjections) { var collection = GetCollection(); - var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; - var findOptions = new FindOptions { TranslationOptions = translationOptions }; + var findOptions = GetFindOptions(useFindOptions, useFindOptions, enableClientSideProjections); var find = collection .Find("{}", findOptions) @@ -80,7 +87,14 @@ public void Find_with_client_side_projection_First_should_work( if (enableClientSideProjections ?? false) { var projection = TranslateFindProjection(collection, find, out var projectionSerializer); - projection.Should().BeNull(); + if (findOptions.TranslationOptions.CompatibilityLevel == ServerVersion.Server42) + { + projection.Should().BeNull(); + } + else + { + projection.Should().Be("{ _snippets : ['$X'], _id : 0 }"); + } projectionSerializer.Should().BeAssignableTo(); var result = find.First(); @@ -106,8 +120,7 @@ public void Find_with_client_side_projection_Single_should_work( bool? enableClientSideProjections) { var collection = GetCollection(); - var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; - var findOptions = new FindOptions { TranslationOptions = translationOptions }; + var findOptions = GetFindOptions(useFindOptions, useFindOptions, enableClientSideProjections); var find = collection .Find("{}", findOptions) @@ -116,7 +129,14 @@ public void Find_with_client_side_projection_Single_should_work( if (enableClientSideProjections ?? false) { var projection = TranslateFindProjection(collection, find, out var projectionSerializer); - projection.Should().BeNull(); + if (findOptions.TranslationOptions.CompatibilityLevel == ServerVersion.Server42) + { + projection.Should().BeNull(); + } + else + { + projection.Should().Be("{ _snippets : ['$X'], _id : 0 }"); + } projectionSerializer.Should().BeAssignableTo(); var result = find.Single(); @@ -150,7 +170,14 @@ public void Aggregate_Project_with_client_side_projection_ToList_should_work( if (enableClientSideProjections ?? false) { var stages = Translate(collection, aggregate, out var outputSerializer); - AssertStages(stages, Array.Empty()); + if (aggregateOptions.TranslationOptions.CompatibilityLevel == ServerVersion.Server42) + { + stages.Should().BeEmpty(); + } + else + { + AssertStages(stages, "{ $project : { _snippets : ['$X'], _id : 0 } }"); + } outputSerializer.Should().BeAssignableTo(); var results = aggregate.ToList(); @@ -184,7 +211,14 @@ public void Aggregate_Project_with_client_side_projection_First_should_work( if (enableClientSideProjections ?? false) { var stages = Translate(collection, aggregate, out var serializer); - AssertStages(stages, Array.Empty()); + if (aggregateOptions.TranslationOptions.CompatibilityLevel == ServerVersion.Server42) + { + stages.Should().BeEmpty(); + } + else + { + AssertStages(stages, "{ $project : { _snippets : ['$X'], _id : 0 } }"); + } serializer.Should().BeAssignableTo(); var result = aggregate.First(); @@ -249,7 +283,14 @@ public void Queryable_Select_with_client_side_projection_ToList_should_work( if (enableClientSideProjections ?? false) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + if (aggregateOptions.TranslationOptions.CompatibilityLevel == ServerVersion.Server42) + { + stages.Should().BeEmpty(); + } + else + { + AssertStages(stages, "{ $project : { _snippets : ['$X'], _id : 0 } }"); + } outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -283,7 +324,14 @@ public void Queryable_Select_with_client_side_projection_First_should_work( if (enableClientSideProjections ?? false) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + if (aggregateOptions.TranslationOptions.CompatibilityLevel == ServerVersion.Server42) + { + stages.Should().BeEmpty(); + } + else + { + AssertStages(stages, "{ $project : { _snippets : ['$X'], _id : 0 } }"); + } outputSerializer.Should().BeAssignableTo(); var result = queryable.First(); @@ -347,7 +395,14 @@ public void Queryable_Select_with_client_side_projection_Single_should_work( if (enableClientSideProjections ?? false) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + if (aggregateOptions.TranslationOptions.CompatibilityLevel == ServerVersion.Server42) + { + stages.Should().BeEmpty(); + } + else + { + AssertStages(stages, "{ $project : { _snippets : ['$X'], _id : 0 } }"); + } outputSerializer.Should().BeAssignableTo(); var result = queryable.Single(); @@ -458,7 +513,7 @@ private AggregateOptions GetAggregateOptions(bool useFindOptions, bool useTransl { (false, _) => null, (true, false) => new AggregateOptions { TranslationOptions = null }, - (true, true) => new AggregateOptions { TranslationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections } }, + (true, true) => new AggregateOptions { TranslationOptions = GetTranslationOptions(enableClientSideProjections) }, }; private IMongoCollection GetCollection() @@ -470,13 +525,26 @@ private IMongoCollection GetCollection() return collection; } - private FindOptions GetFindOptions(bool useFindOptions, bool useTranslationOptions, bool? enableClientSideProjections) => - (useFindOptions, useTranslationOptions) switch + private FindOptions GetFindOptions(bool useFindOptions, bool useTranslationOptions, bool? enableClientSideProjections) + { + return (useFindOptions, useTranslationOptions) switch { (false, _) => null, (true, false) => new FindOptions { TranslationOptions = null }, - (true, true) => new FindOptions { TranslationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections } }, + (true, true) => new FindOptions { TranslationOptions = GetTranslationOptions(enableClientSideProjections) } + }; + } + + private ExpressionTranslationOptions GetTranslationOptions(bool? enableClientSideProjections) + { + var wireVersion = CoreTestConfiguration.MaxWireVersion; + var compatibilityLevel = Feature.FindProjectionExpressions.IsSupported(wireVersion) ? (ServerVersion?)null : ServerVersion.Server42; + return new ExpressionTranslationOptions + { + EnableClientSideProjections = enableClientSideProjections, + CompatibilityLevel = compatibilityLevel }; + } private int MyFunction(int x) => 2 * x; diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4813Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4813Tests.cs index 20f402071bd..43e145b1354 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4813Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4813Tests.cs @@ -20,6 +20,8 @@ using FluentAssertions; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Options; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; using MongoDB.TestHelpers.XunitExtensions; using Xunit; @@ -174,6 +176,7 @@ public void Where_ListInterface_Count_should_work() public void Select_BitArray_Count_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -183,7 +186,7 @@ public void Select_BitArray_Count_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$BitArray'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -217,6 +220,7 @@ public void Select_Count_should_work() public void Select_Dictionary_Count_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -226,7 +230,7 @@ public void Select_Dictionary_Count_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$Dictionary'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -275,6 +279,7 @@ public void Select_DictionaryAsArrayOfDocuments_Count_should_work() public void Select_DictionaryInterface_Count_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -284,7 +289,7 @@ public void Select_DictionaryInterface_Count_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$DictionaryInterface'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4957Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4957Tests.cs index 94b733ba3f3..ae8b8c7d7b8 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4957Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4957Tests.cs @@ -18,6 +18,8 @@ using FluentAssertions; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; using MongoDB.TestHelpers.XunitExtensions; using Xunit; @@ -79,6 +81,7 @@ public void New_array_with_two_items_should_work() public void New_array_with_two_items_with_different_serializers_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -88,7 +91,7 @@ public void New_array_with_two_items_with_different_serializers_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$X', '$Y'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var result = queryable.Single(); diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5043Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5043Tests.cs index 01da168a631..76762286d81 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5043Tests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5043Tests.cs @@ -18,6 +18,8 @@ using FluentAssertions; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; using MongoDB.TestHelpers.XunitExtensions; using Xunit; @@ -168,6 +170,7 @@ public void Convert_ES1_to_E1_should_work() public void Convert_ES1_to_E2_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -177,7 +180,7 @@ public void Convert_ES1_to_E2_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$ES1'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); @@ -211,6 +214,7 @@ public void Convert_ES1_to_nullable_E1_should_work() public void Convert_ES1_to_nullable_E2_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = GetCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -220,7 +224,7 @@ public void Convert_ES1_to_nullable_E2_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$ES1'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList(); diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5321Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5321Tests.cs new file mode 100644 index 00000000000..f06fad4544b --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5321Tests.cs @@ -0,0 +1,101 @@ +/* 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.Linq; +using FluentAssertions; +using MongoDB.Driver.Core.Misc; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira +{ + public class CSharp5321Tests : Linq3IntegrationTest + { + [Fact] + public void Client_side_projection_should_fetch_only_needed_fields() + { + var collection = GetCollection(); + var translationOptions = GetTranslationOptions(); + + var queryable = collection.AsQueryable(translationOptions) + .Select(x => Add(x.X, 1)); + + var stages = Translate(collection, queryable); + if (translationOptions.CompatibilityLevel == ServerVersion.Server42) + { + stages.Should().BeEmpty(); + } + else + { + AssertStages(stages, "{ $project : { _snippets : ['$X'], _id : 0 } }"); + } + + var result = queryable.Single(); + result.Should().Be(2); + } + + [Fact] + public void Client_side_projection_should_compute_sum_server_side() + { + var collection = GetCollection(); + var translationOptions = GetTranslationOptions(); + + var queryable = collection.AsQueryable(translationOptions) + .Select(x => Add(x.A.Sum(), 4)); + + var stages = Translate(collection, queryable); + if (translationOptions.CompatibilityLevel == ServerVersion.Server42) + { + stages.Should().BeEmpty(); + } + else + { + AssertStages(stages, "{ $project : { _snippets : [{ $sum : '$A' }], _id : 0 } }"); + } + + var result = queryable.Single(); + result.Should().Be(10); + } + + private IMongoCollection GetCollection() + { + var collection = GetCollection("test"); + CreateCollection( + collection, + new C { Id = 1, X = 1, A = [1, 2, 3] }); + return collection; + } + + private ExpressionTranslationOptions GetTranslationOptions() + { + var wireVersion = CoreTestConfiguration.MaxWireVersion; + var compatibilityLevel = Feature.FindProjectionExpressions.IsSupported(wireVersion) ? (ServerVersion?)null : ServerVersion.Server42; + return new ExpressionTranslationOptions + { + EnableClientSideProjections = true, + CompatibilityLevel = compatibilityLevel, + }; + } + + private static int Add(int x, int y) => x + y; + + private class C + { + public int Id { get; set; } + public int X { get; set; } + public int[] A { get; set; } + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/NegateExpressionToAggregationExpressionTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/NegateExpressionToAggregationExpressionTranslatorTests.cs index 9c3d3952cd1..b7a7683eba7 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/NegateExpressionToAggregationExpressionTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/NegateExpressionToAggregationExpressionTranslatorTests.cs @@ -18,6 +18,8 @@ using FluentAssertions; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; using MongoDB.TestHelpers.XunitExtensions; using Xunit; @@ -106,6 +108,7 @@ public void Negate_decimal128_should_work() public void Negate_decimal_as_string_should_throw( [Values(false, true)] bool enableClientSideProjections) { + RequireServer.Check().Supports(Feature.FindProjectionExpressions); var collection = CreateCollection(); var translationOptions = new ExpressionTranslationOptions { EnableClientSideProjections = enableClientSideProjections }; @@ -114,7 +117,7 @@ public void Negate_decimal_as_string_should_throw( if (enableClientSideProjections) { var stages = Translate(collection, queryable, out var outputSerializer); - AssertStages(stages, Array.Empty()); + AssertStages(stages, "{ $project : { _snippets : ['$DecimalAsString'], _id : 0 } }"); outputSerializer.Should().BeAssignableTo(); var results = queryable.ToList();