Skip to content

Commit 4aadc9f

Browse files
committed
CSHARP-4666: Support projecting a single field with Find on servers prior to 4.4.
1 parent cf5009b commit 4aadc9f

File tree

6 files changed

+155
-47
lines changed

6 files changed

+155
-47
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/LinqProviderAdapterV3.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@
1414
*/
1515

1616
using System;
17+
using System.Collections.Generic;
1718
using System.Linq;
1819
using System.Linq.Expressions;
1920
using MongoDB.Bson;
2021
using MongoDB.Bson.Serialization;
2122
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Optimizers;
23+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Stages;
2224
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
2325
using MongoDB.Driver.Linq.Linq3Implementation.Translators;
2426
using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators;
@@ -139,7 +141,7 @@ internal override RenderedProjectionDefinition<TProjection> TranslateExpressionT
139141
Expression<Func<TSource, TProjection>> expression,
140142
IBsonSerializer<TSource> sourceSerializer,
141143
IBsonSerializerRegistry serializerRegistry)
142-
=> TranslateExpressionToProjectionInternal(expression, sourceSerializer, new AstFindProjectionSimplifier());
144+
=> TranslateExpressionToProjection(expression, sourceSerializer, ProjectionHelper.CreateFindProjection, new AstFindProjectionSimplifier());
143145

144146
internal override RenderedProjectionDefinition<TOutput> TranslateExpressionToGroupProjection<TInput, TKey, TOutput>(
145147
Expression<Func<TInput, TKey>> idExpression,
@@ -156,20 +158,20 @@ internal override RenderedProjectionDefinition<TOutput> TranslateExpressionToPro
156158
IBsonSerializer<TInput> inputSerializer,
157159
IBsonSerializerRegistry serializerRegistry,
158160
ExpressionTranslationOptions translationOptions)
159-
=> TranslateExpressionToProjectionInternal(expression, inputSerializer, new AstSimplifier());
161+
=> TranslateExpressionToProjection(expression, inputSerializer, ProjectionHelper.CreateAggregationProjection, new AstSimplifier());
160162

161-
private RenderedProjectionDefinition<TOutput> TranslateExpressionToProjectionInternal<TInput, TOutput>(
163+
private RenderedProjectionDefinition<TOutput> TranslateExpressionToProjection<TInput, TOutput>(
162164
Expression<Func<TInput, TOutput>> expression,
163165
IBsonSerializer<TInput> inputSerializer,
166+
Func<AggregationExpression, (IReadOnlyList<AstProjectStageSpecification>, IBsonSerializer)> projectionCreator,
164167
AstSimplifier simplifier)
165168
{
166169
expression = (Expression<Func<TInput, TOutput>>)PartialEvaluator.EvaluatePartially(expression);
167170
var context = TranslationContext.Create(expression, inputSerializer);
168171
var translation = ExpressionToAggregationExpressionTranslator.TranslateLambdaBody(context, expression, inputSerializer, asRoot: true);
169-
var (projectStage, projectionSerializer) = ProjectionHelper.CreateProjectStage(translation);
170-
var simplifiedProjectStage = simplifier.Visit(projectStage);
171-
var renderedProjection = simplifiedProjectStage.Render().AsBsonDocument["$project"].AsBsonDocument;
172-
172+
var (specifications, projectionSerializer) = projectionCreator(translation);
173+
specifications = simplifier.VisitAndConvert(specifications);
174+
var renderedProjection = new BsonDocument(specifications.Select(specification => specification.RenderAsElement()));
173175
return new RenderedProjectionDefinition<TOutput>(renderedProjection, (IBsonSerializer<TOutput>)projectionSerializer);
174176
}
175177
}

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

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -27,54 +27,95 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Misc
2727
internal static class ProjectionHelper
2828
{
2929
// public static methods
30-
public static (AstProjectStage, IBsonSerializer) CreateProjectStage(AggregationExpression expression)
30+
public static (IReadOnlyList<AstProjectStageSpecification>, IBsonSerializer) CreateAggregationProjection(AggregationExpression expression)
3131
{
32-
if (expression.Ast.NodeType == AstNodeType.ComputedDocumentExpression)
32+
return expression.Ast.NodeType switch
3333
{
34-
return CreateComputedDocumentProjectStage(expression);
35-
}
36-
else
34+
AstNodeType.ComputedDocumentExpression => CreateComputedDocumentProjection(expression),
35+
_ => CreateWrappedValueProjection(expression)
36+
};
37+
}
38+
39+
public static (IReadOnlyList<AstProjectStageSpecification>, IBsonSerializer) CreateFindProjection(AggregationExpression expression)
40+
{
41+
return expression.Ast.NodeType switch
3742
{
38-
return CreateWrappedValueProjectStage(expression);
39-
}
43+
AstNodeType.GetFieldExpression => CreateFindGetFieldProjection(expression),
44+
_ => CreateAggregationProjection(expression)
45+
};
46+
}
47+
48+
public static (AstProjectStage, IBsonSerializer) CreateProjectStage(AggregationExpression expression)
49+
{
50+
var (specifications, projectionSerializer) = CreateAggregationProjection(expression);
51+
var projectStage = AstStage.Project(specifications);
52+
return (projectStage, projectionSerializer);
4053
}
4154

4255
// private static methods
43-
private static (AstProjectStage, IBsonSerializer) CreateComputedDocumentProjectStage(AggregationExpression expression)
56+
private static (IReadOnlyList<AstProjectStageSpecification>, IBsonSerializer) CreateComputedDocumentProjection(AggregationExpression expression)
4457
{
4558
var computedDocument = (AstComputedDocumentExpression)expression.Ast;
4659

4760
var specifications = new List<AstProjectStageSpecification>();
48-
4961
var isIdProjected = false;
5062
foreach (var computedField in computedDocument.Fields)
5163
{
5264
var path = computedField.Path;
53-
var value = computedField.Value;
54-
55-
if (value is AstConstantExpression astConstantExpression)
56-
{
57-
if (ValueNeedsToBeQuoted(astConstantExpression.Value))
58-
{
59-
value = AstExpression.Literal(value);
60-
}
61-
}
65+
var value = QuoteIfNecessary(computedField.Value);
6266
specifications.Add(AstProject.Set(path, value));
6367
isIdProjected |= path == "_id";
6468
}
65-
6669
if (!isIdProjected)
6770
{
6871
specifications.Add(AstProject.ExcludeId());
6972
}
7073

71-
var projectStage = AstStage.Project(specifications);
74+
return (specifications, expression.Serializer);
75+
}
7276

73-
return (projectStage, expression.Serializer);
77+
private static (IReadOnlyList<AstProjectStageSpecification>, IBsonSerializer) CreateFindGetFieldProjection(AggregationExpression expression)
78+
{
79+
var getFieldExpressionAst = (AstGetFieldExpression)expression.Ast;
80+
if (getFieldExpressionAst.HasSafeFieldName(out var fieldName))
81+
{
82+
var specifications = fieldName == "_id" ?
83+
new List<AstProjectStageSpecification> { AstProject.Include(fieldName) } :
84+
new List<AstProjectStageSpecification> { AstProject.Include(fieldName), AstProject.Exclude("_id") };
85+
var wrappedValueSerializer = WrappedValueSerializer.Create(fieldName, expression.Serializer);
86+
return (specifications, wrappedValueSerializer);
87+
}
7488

75-
bool ValueNeedsToBeQuoted(BsonValue constantValue)
89+
return CreateWrappedValueProjection(expression);
90+
}
91+
92+
private static (IReadOnlyList<AstProjectStageSpecification>, IBsonSerializer) CreateWrappedValueProjection(AggregationExpression expression)
93+
{
94+
var wrappedValueSerializer = WrappedValueSerializer.Create("_v", expression.Serializer);
95+
var specifications = new List<AstProjectStageSpecification>
96+
{
97+
AstProject.Set(wrappedValueSerializer.FieldName, QuoteIfNecessary(expression.Ast)),
98+
AstProject.ExcludeId()
99+
};
100+
101+
return (specifications, wrappedValueSerializer);
102+
}
103+
104+
private static AstExpression QuoteIfNecessary(AstExpression expression)
105+
{
106+
if (expression is AstConstantExpression constantExpression)
107+
{
108+
if (ValueNeedsToBeQuoted(constantExpression.Value))
109+
{
110+
return AstExpression.Literal(constantExpression);
111+
}
112+
}
113+
114+
return expression;
115+
116+
bool ValueNeedsToBeQuoted(BsonValue value)
76117
{
77-
switch (constantValue.BsonType)
118+
switch (value.BsonType)
78119
{
79120
case BsonType.Boolean:
80121
case BsonType.Decimal128:
@@ -88,16 +129,5 @@ bool ValueNeedsToBeQuoted(BsonValue constantValue)
88129
}
89130
}
90131
}
91-
92-
private static (AstProjectStage, IBsonSerializer) CreateWrappedValueProjectStage(AggregationExpression expression)
93-
{
94-
var wrappedValueSerializer = WrappedValueSerializer.Create("_v", expression.Serializer);
95-
var projectStage =
96-
AstStage.Project(
97-
AstProject.Set("_v", expression.Ast),
98-
AstProject.ExcludeId());
99-
100-
return (projectStage, wrappedValueSerializer);
101-
}
102132
}
103133
}

tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4062Tests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public void Project_boolean_constant_should_work()
5454

5555
var stages = Translate(collection, aggregate);
5656

57-
AssertStages(stages, "{ $project : { _v : true, _id : 0 } }");
57+
AssertStages(stages, "{ $project : { _v : { $literal : true }, _id : 0 } }");
5858
}
5959

6060
[Fact]
@@ -66,7 +66,7 @@ public void Project_int_constant_should_work()
6666

6767
var stages = Translate(collection, aggregate);
6868

69-
AssertStages(stages, "{ $project : { _v : 1, _id : 0 } }");
69+
AssertStages(stages, "{ $project : { _v : { $literal : 1 }, _id : 0 } }");
7070
}
7171

7272
[Fact]
@@ -102,7 +102,7 @@ public void Select_boolean_constant_should_work()
102102

103103
var stages = Translate(collection, queryable);
104104

105-
AssertStages(stages, "{ $project : { _v : true, _id : 0 } }");
105+
AssertStages(stages, "{ $project : { _v : { $literal : true }, _id : 0 } }");
106106
}
107107

108108
[Fact]
@@ -114,7 +114,7 @@ public void Select_int_constant_should_work()
114114

115115
var stages = Translate(collection, queryable);
116116

117-
AssertStages(stages, "{ $project : { _v : 1, _id : 0 } }");
117+
AssertStages(stages, "{ $project : { _v : { $literal : 1 }, _id : 0 } }");
118118
}
119119

120120
private class C

tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4538Tests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public void Project_with_nullable_value_should_work()
3535
.Project(x => x.D);
3636

3737
var projection = TranslateFindProjection(collection, find);
38-
projection.Should().Be("{ _v : '$D', _id : 0 }");
38+
projection.Should().Be("{ D : 1, _id : 0 }");
3939

4040
var results = find.ToList();
4141
results.Should().Equal(1.0, null, null);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 FluentAssertions;
17+
using MongoDB.Driver.Linq;
18+
using MongoDB.TestHelpers.XunitExtensions;
19+
using Xunit;
20+
21+
namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira
22+
{
23+
public class CSharp4666Tests : Linq3IntegrationTest
24+
{
25+
[Theory]
26+
[ParameterAttributeData]
27+
public void Find_project_id_should_work(
28+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
29+
{
30+
var collection = GetCollection(linqProvider);
31+
32+
var find = collection
33+
.Find(_ => true)
34+
.Project(x => x.Id);
35+
36+
var projection = TranslateFindProjection(collection, find);
37+
projection.Should().Be("{ _id : 1 }");
38+
39+
var result = find.Single();
40+
result.Should().Be(1);
41+
}
42+
43+
[Theory]
44+
[ParameterAttributeData]
45+
public void Find_project_field_should_work(
46+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
47+
{
48+
var collection = GetCollection(linqProvider);
49+
50+
var find = collection
51+
.Find(_ => true)
52+
.Project(x => x.X);
53+
54+
var projection = TranslateFindProjection(collection, find);
55+
projection.Should().Be("{ X : 1, _id : 0 }");
56+
57+
var result = find.Single();
58+
result.Should().Be(2);
59+
}
60+
61+
private IMongoCollection<C> GetCollection(LinqProvider linqProvider)
62+
{
63+
var collection = GetCollection<C>("test", linqProvider);
64+
CreateCollection(
65+
collection,
66+
new C { Id = 1, X = 2 });
67+
return collection;
68+
}
69+
70+
private class C
71+
{
72+
public int Id { get; set; }
73+
public int X { get; set; }
74+
}
75+
}
76+
}

tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/LinqProviderV3Tests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public void TranslateExpressionToFindProjection_should_return_expected_result()
134134

135135
var result = subject.TranslateExpressionToFindProjection(expression, documentSerializer, serializerRegistry);
136136

137-
result.Document.Should().Be("{ _v : '$X', _id : 0 }");
137+
result.Document.Should().Be("{ X : 1, _id : 0 }");
138138
result.ProjectionSerializer.ValueType.Should().Be(typeof(int));
139139
}
140140

0 commit comments

Comments
 (0)