Skip to content

Commit 2ab2868

Browse files
committed
CSHARP-5162: Continue to use FindExpressionProjectionDefinition to avoid regression against LINQ2.
1 parent e0849de commit 2ab2868

File tree

5 files changed

+251
-19
lines changed

5 files changed

+251
-19
lines changed

src/MongoDB.Driver/ProjectionDefinition.cs

+15
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,21 @@ public Expression<Func<TSource, TProjection>> Expression
306306

307307
/// <inheritdoc />
308308
public override RenderedProjectionDefinition<TProjection> Render(IBsonSerializer<TSource> sourceSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider)
309+
{
310+
if (linqProvider == LinqProvider.V2)
311+
{
312+
// this is slightly wrong because we're not actually rendering for a Find
313+
// but this is required to avoid a regression with LINQ2
314+
return RenderForFind(sourceSerializer, serializerRegistry, linqProvider);
315+
}
316+
else
317+
{
318+
return linqProvider.GetAdapter().TranslateExpressionToProjection(_expression, sourceSerializer, serializerRegistry, translationOptions: null);
319+
}
320+
}
321+
322+
/// <inheritdoc />
323+
internal override RenderedProjectionDefinition<TProjection> RenderForFind(IBsonSerializer<TSource> sourceSerializer, IBsonSerializerRegistry serializerRegistry, LinqProvider linqProvider)
309324
{
310325
return linqProvider.GetAdapter().TranslateExpressionToFindProjection(_expression, sourceSerializer, serializerRegistry);
311326
}

src/MongoDB.Driver/ProjectionDefinitionBuilder.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,9 @@ public ProjectionDefinition<TSource> Exclude(Expression<Func<TSource, object>> f
544544
/// </returns>
545545
public ProjectionDefinition<TSource, TProjection> Expression<TProjection>(Expression<Func<TSource, TProjection>> expression)
546546
{
547-
return new ExpressionProjectionDefinition<TSource, TProjection>(expression, null);
547+
// TODO: replace FindExpressionProjectionDefinition with ExpressionProjectionDefinition when LINQ2 is removed
548+
// in the meantime we have to keep using FindExpressionProjectionDefinition here for compatibility with LINQ2
549+
return new FindExpressionProjectionDefinition<TSource, TProjection>(expression);
548550
}
549551

550552
/// <summary>

tests/MongoDB.Driver.Tests/FindExpressionProjectionDefinitionTests.cs

+47-13
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,69 @@
1717
using System.Linq.Expressions;
1818
using FluentAssertions;
1919
using MongoDB.Bson.Serialization;
20+
using MongoDB.Driver.Linq;
21+
using MongoDB.TestHelpers.XunitExtensions;
2022
using Xunit;
2123

2224
namespace MongoDB.Driver.Tests
2325
{
2426
public class FindExpressionProjectionDefinitionTests
2527
{
26-
[Fact]
27-
public void Projection_to_class_should_work()
28-
=> AssertProjection(
28+
[Theory]
29+
[ParameterAttributeData]
30+
public void Projection_to_class_should_work(
31+
[Values(false, true)] bool renderForFind,
32+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
33+
{
34+
var expectedRenderedProjection = (linqProvider, renderForFind) switch
35+
{
36+
(LinqProvider.V2, _) => "{ A : 1, B : 1, _id : 0 }", // note: result serializer does client-side projection
37+
(LinqProvider.V3, true) => "{ A : 1, X : '$B', _id : 0 }",
38+
(LinqProvider.V3, false) => "{ A : '$A', X : '$B', _id : 0 }",
39+
_ => throw new Exception()
40+
};
41+
AssertProjection(
2942
x => new Projection { A = x.A, X = x.B },
30-
"{ A : 1, X : '$B', _id : 0 }");
43+
renderForFind,
44+
linqProvider,
45+
expectedRenderedProjection);
46+
}
3147

32-
[Fact]
33-
public void Projection_to_anonymous_type_should_work()
34-
=> AssertProjection(
48+
[Theory]
49+
[ParameterAttributeData]
50+
public void Projection_to_anonymous_type_should_work(
51+
[Values(false, true)] bool renderForFind,
52+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
53+
{
54+
var expectedRenderedProjection = (linqProvider, renderForFind) switch
55+
{
56+
(LinqProvider.V2, _) => "{ A : 1, B : 1, _id : 0 }", // note: result serializer does client-side projection
57+
(LinqProvider.V3, true) => "{ A : 1, X : '$B', _id : 0 }",
58+
(LinqProvider.V3, false) => "{ A : '$A', X : '$B', _id : 0 }",
59+
_ => throw new Exception()
60+
};
61+
AssertProjection(
3562
x => new { x.A, X = x.B },
36-
"{ A : 1, X : '$B', _id : 0 }");
63+
renderForFind,
64+
linqProvider,
65+
expectedRenderedProjection);
66+
}
3767

3868
private void AssertProjection<TProjection>(
3969
Expression<Func<Document, TProjection>> expression,
40-
string expectedProjection)
70+
bool renderForFind,
71+
LinqProvider linqProvider,
72+
string expectedRenderedProjection)
4173
{
4274
var projection = new FindExpressionProjectionDefinition<Document, TProjection>(expression);
75+
var serializerRegistry = BsonSerializer.SerializerRegistry;
76+
var documentSerializer = serializerRegistry.GetSerializer<Document>();
4377

44-
var renderedProjection = projection.Render(
45-
BsonSerializer.LookupSerializer<Document>(),
46-
BsonSerializer.SerializerRegistry);
78+
var renderedProjection = renderForFind ?
79+
projection.RenderForFind(documentSerializer, serializerRegistry, linqProvider) :
80+
projection.Render(documentSerializer, serializerRegistry, linqProvider);
4781

48-
renderedProjection.Document.Should().BeEquivalentTo(expectedProjection);
82+
renderedProjection.Document.Should().BeEquivalentTo(expectedRenderedProjection);
4983
}
5084

5185
private class Document

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

+2-5
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,8 @@ public void Find_projection_render_should_work(
3232

3333
var fluentFind = collection.Find(a => a.Id == "1").Project(a => a.Id);
3434

35-
var documentSerializer = collection.DocumentSerializer;
36-
var serializerRegistry = BsonSerializer.SerializerRegistry;
37-
var renderedProjection = fluentFind.Options.Projection.Render(documentSerializer, serializerRegistry, linqProvider);
38-
39-
renderedProjection.Document.Should().Be("{ _id : 1 }");
35+
var renderedProjection = TranslateFindProjection(collection, fluentFind);
36+
renderedProjection.Should().Be("{ _id : 1 }");
4037

4138
var result = fluentFind.Single();
4239
result.Should().Be("1");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using FluentAssertions;
20+
using MongoDB.Bson;
21+
using MongoDB.Bson.Serialization.Attributes;
22+
using MongoDB.Driver.Linq;
23+
using MongoDB.TestHelpers.XunitExtensions;
24+
using Xunit;
25+
26+
namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira
27+
{
28+
public class CSharp5162Tests : Linq3IntegrationTest
29+
{
30+
[Theory]
31+
[ParameterAttributeData]
32+
public void Builders_Projection_Expression_with_camel_casing_should_work(
33+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
34+
{
35+
var collection = GetCamelCollection(linqProvider);
36+
37+
var projection = Builders<CamelDocument>.Projection.Expression(x => new CamelDocument { Id = x.Id, Name = x.Name });
38+
var aggregate = collection.Aggregate().Project(projection);
39+
40+
var stages = Translate(collection, aggregate);
41+
if (linqProvider == LinqProvider.V2)
42+
{
43+
AssertStages(stages, "{ $project : { _id : 1, name : 1 } }");
44+
}
45+
else
46+
{
47+
AssertStages(stages, "{ $project : { _id : '$_id', name : '$name' } }");
48+
}
49+
50+
var result = aggregate.ToList().Single();
51+
result.Id.Should().Be(1);
52+
result.Name.Should().Be("John Doe");
53+
}
54+
55+
[Theory]
56+
[ParameterAttributeData]
57+
public void Builders_Projection_Expression_with_pascal_casing_should_work(
58+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
59+
{
60+
var collection = GetPascalCollection(linqProvider);
61+
62+
var projection = Builders<PascalDocument>.Projection.Expression(x => new PascalDocument { Id = x.Id, Name = x.Name });
63+
var aggregate = collection.Aggregate().Project(projection);
64+
65+
var stages = Translate(collection, aggregate);
66+
if (linqProvider == LinqProvider.V2)
67+
{
68+
stages = NormalizeProjectFieldOrder(stages);
69+
AssertStages(stages, "{ $project : { _id : 1, Name : 1 } }");
70+
}
71+
else
72+
{
73+
AssertStages(stages, "{ $project : { _id : '$_id', Name : '$Name' } }");
74+
}
75+
76+
var result = aggregate.ToList().Single();
77+
result.Id.Should().Be(1);
78+
result.Name.Should().Be("John Doe");
79+
}
80+
81+
[Theory]
82+
[ParameterAttributeData]
83+
public void FindExpressionDefinition_with_camel_casing_should_work(
84+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
85+
{
86+
var collection = GetCamelCollection(linqProvider);
87+
88+
var projection = new FindExpressionProjectionDefinition<CamelDocument, CamelDocument>(x => new CamelDocument { Id = x.Id, Name = x.Name });
89+
var aggregate = collection.Aggregate().Project(projection);
90+
91+
var stages = Translate(collection, aggregate);
92+
if (linqProvider == LinqProvider.V2)
93+
{
94+
AssertStages(stages, "{ $project : { _id : 1, name : 1 } }");
95+
}
96+
else
97+
{
98+
AssertStages(stages, "{ $project : { _id : '$_id', name : '$name' } }");
99+
}
100+
101+
var result = aggregate.ToList().Single();
102+
result.Id.Should().Be(1);
103+
result.Name.Should().Be("John Doe");
104+
}
105+
106+
[Theory]
107+
[ParameterAttributeData]
108+
public void FindExpressionDefinition_with_pascal_casing_should_work(
109+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
110+
{
111+
var collection = GetPascalCollection(linqProvider);
112+
113+
var projection = new FindExpressionProjectionDefinition<PascalDocument, PascalDocument>(x => new PascalDocument { Id = x.Id, Name = x.Name });
114+
var aggregate = collection.Aggregate().Project(projection);
115+
116+
var stages = Translate(collection, aggregate);
117+
if (linqProvider == LinqProvider.V2)
118+
{
119+
stages = NormalizeProjectFieldOrder(stages);
120+
AssertStages(stages, "{ $project : { _id : 1, Name : 1 } }");
121+
}
122+
else
123+
{
124+
AssertStages(stages, "{ $project : { _id : '$_id', Name : '$Name' } }");
125+
}
126+
127+
var result = aggregate.ToList().Single();
128+
result.Id.Should().Be(1);
129+
result.Name.Should().Be("John Doe");
130+
}
131+
132+
private IMongoCollection<CamelDocument> GetCamelCollection(LinqProvider linqProvider)
133+
{
134+
var collection = GetCollection<CamelDocument>("test", linqProvider);
135+
var document = new CamelDocument { Id = 1, Name = "John Doe" };
136+
CreateCollection(collection, document);
137+
return collection;
138+
}
139+
140+
private IMongoCollection<PascalDocument> GetPascalCollection(LinqProvider linqProvider)
141+
{
142+
var collection = GetCollection<PascalDocument>("test", linqProvider);
143+
var document = new PascalDocument { Id = 1, Name = "John Doe" };
144+
CreateCollection(collection, document);
145+
return collection;
146+
}
147+
148+
private List<BsonDocument> NormalizeProjectFieldOrder(List<BsonDocument> stages)
149+
{
150+
if (stages.Count == 1 &&
151+
stages[0] is BsonDocument projectStage &&
152+
projectStage.ElementCount == 1 &&
153+
projectStage.GetElement(0).Name == "$project" &&
154+
projectStage[0] is BsonDocument projection &&
155+
projection.ElementCount == 2 &&
156+
projection.Names.SequenceEqual(["Name", "_id"]))
157+
{
158+
stages[0]["$project"] = new BsonDocument
159+
{
160+
{ "_id", projection["_id"] },
161+
{ "Name", projection["Name"] }
162+
};
163+
}
164+
165+
return stages;
166+
}
167+
168+
private class CamelDocument
169+
{
170+
public int Id { get; set; }
171+
[BsonElement("name")] public string Name { get; set; }
172+
[BsonElement("activeSince")] public DateTime ActiveSince { get; set; }
173+
[BsonElement("isActive")] public bool IsActive { get; set; }
174+
}
175+
176+
private class PascalDocument
177+
{
178+
public int Id { get; set; }
179+
public string Name { get; set; }
180+
public DateTime ActiveSince { get; set; }
181+
public bool IsActive { get; set; }
182+
}
183+
}
184+
}

0 commit comments

Comments
 (0)