Skip to content

Commit c9e1e28

Browse files
sanych-sunrstam
authored andcommitted
CSHARP-4627: Support Union via $unionWith in LINQ3 (#1078)
1 parent 8c4466d commit c9e1e28

File tree

6 files changed

+223
-2
lines changed

6 files changed

+223
-2
lines changed

src/MongoDB.Driver/Linq/IMongoQueryable.cs

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

1616
using System.Linq;
17-
using MongoDB.Bson.Serialization;
1817

1918
namespace MongoDB.Driver.Linq
2019
{

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,28 @@
1313
* limitations under the License.
1414
*/
1515

16+
using System;
1617
using System.Linq.Expressions;
1718
using MongoDB.Bson.Serialization;
1819

1920
namespace MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods
2021
{
2122
internal static class ExpressionExtensions
2223
{
24+
public static object Evaluate(this Expression expression)
25+
{
26+
if (expression is ConstantExpression constantExpression)
27+
{
28+
return constantExpression.Value;
29+
}
30+
else
31+
{
32+
LambdaExpression lambda = Expression.Lambda(expression);
33+
Delegate fn = lambda.Compile();
34+
return fn.DynamicInvoke(null);
35+
}
36+
}
37+
2338
public static (string CollectionName, IBsonSerializer DocumentSerializer) GetCollectionInfo(this Expression innerExpression, Expression containerExpression)
2439
{
2540
if (innerExpression is ConstantExpression constantExpression &&

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/ExpressionToExecutableQueryTranslator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public static ExecutableQuery<TDocument, TResult> TranslateScalar<TDocument, TRe
5454
case "AverageAsync":
5555
return AverageMethodToExecutableQueryTranslator<TResult>.Translate(provider, context, methodCallExpression);
5656
case "Contains":
57-
return ContainsMethodToExecutableQueryTranslator.Translate(provider, context, methodCallExpression).AsExecutableQuery<TDocument, TResult>(); ;
57+
return ContainsMethodToExecutableQueryTranslator.Translate(provider, context, methodCallExpression).AsExecutableQuery<TDocument, TResult>();
5858
case "Count":
5959
case "CountAsync":
6060
return CountMethodToExecutableQueryTranslator.Translate(provider, context, methodCallExpression).AsExecutableQuery<TDocument, TResult>();

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/ExpressionToPipelineTranslator.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public static AstPipeline Translate(TranslationContext context, Expression expre
6565
return SkipMethodToPipelineTranslator.Translate(context, methodCallExpression);
6666
case "Take":
6767
return TakeMethodToPipelineTranslator.Translate(context, methodCallExpression);
68+
case "Union":
69+
return UnionMethodToPipelineTranslator.Translate(context, methodCallExpression);
6870
case "Where":
6971
return WhereMethodToPipelineTranslator.Translate(context, methodCallExpression);
7072
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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.Linq.Expressions;
17+
using MongoDB.Driver.Linq.Linq3Implementation.Ast;
18+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Stages;
19+
using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods;
20+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
21+
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
22+
23+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToPipelineTranslators
24+
{
25+
internal static class UnionMethodToPipelineTranslator
26+
{
27+
public static AstPipeline Translate(TranslationContext context, MethodCallExpression expression)
28+
{
29+
var method = expression.Method;
30+
var arguments = expression.Arguments;
31+
32+
if (method.Is(QueryableMethod.Union))
33+
{
34+
var firstExpression = arguments[0];
35+
var pipeline = ExpressionToPipelineTranslator.Translate(context, firstExpression);
36+
37+
var secondExpression = arguments[1];
38+
var secondValue = secondExpression.Evaluate();
39+
if (secondValue is IMongoQueryable secondQueryable)
40+
{
41+
var secondProvider = (IMongoQueryProvider)secondQueryable.Provider;
42+
var secondCollectionName = secondProvider.CollectionNamespace.CollectionName;
43+
var secondPipelineInputSerializer = secondProvider.PipelineInputSerializer;
44+
var secondContext = TranslationContext.Create(secondQueryable.Expression, secondPipelineInputSerializer);
45+
var secondPipeline = ExpressionToPipelineTranslator.Translate(secondContext, secondQueryable.Expression);
46+
if (secondPipeline.Stages.Count == 0)
47+
{
48+
secondPipeline = null;
49+
}
50+
51+
pipeline = pipeline.AddStages(
52+
pipeline.OutputSerializer,
53+
AstStage.UnionWith(secondCollectionName, secondPipeline));
54+
55+
return pipeline;
56+
}
57+
58+
throw new ExpressionNotSupportedException(expression, because: "second argument must be IMongoQueryable");
59+
}
60+
61+
throw new ExpressionNotSupportedException(expression);
62+
}
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
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.Linq;
17+
using FluentAssertions;
18+
using Xunit;
19+
20+
namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Translators.ExpressionToPipelineTranslators
21+
{
22+
public class UnionMethodToPipelineTranslatorTests: Linq3IntegrationTest
23+
{
24+
private readonly IMongoCollection<Company> _firstCollection;
25+
private readonly IMongoCollection<Company> _secondCollection;
26+
27+
public UnionMethodToPipelineTranslatorTests()
28+
{
29+
_firstCollection = CreateCollection("clients",
30+
new Company { Id = 1, Name = "first client" },
31+
new Company { Id = 2, Name = "second client" },
32+
new Company { Id = 3, Name = "third client" });
33+
34+
_secondCollection = CreateCollection("partners",
35+
new Company { Id = 4, Name = "partner" },
36+
new Company { Id = 5, Name = "another partner" });
37+
}
38+
39+
[Fact]
40+
public void Union_should_combine_collections()
41+
{
42+
var queryable = _firstCollection
43+
.AsQueryable()
44+
.Union(_secondCollection.AsQueryable());
45+
46+
var stages = Translate(_firstCollection, queryable);
47+
AssertStages(stages, "{ $unionWith : 'partners' }");
48+
49+
var results = queryable.ToList();
50+
results.Select(x => x.Id).Should().BeEquivalentTo(new[] { 1, 2, 3, 4, 5 });
51+
}
52+
53+
[Fact]
54+
public void Union_should_combine_collection_with_itself()
55+
{
56+
var queryable = _firstCollection
57+
.AsQueryable()
58+
.Union(_firstCollection.AsQueryable());
59+
60+
var stages = Translate(_firstCollection, queryable);
61+
AssertStages(stages, "{ $unionWith : 'clients' }");
62+
63+
var results = queryable.ToList();
64+
results.Select(x => x.Id).Should().BeEquivalentTo(new[] { 1, 2, 3, 1, 2, 3 });
65+
}
66+
67+
[Fact]
68+
public void Union_should_combine_filtered_collections()
69+
{
70+
var queryable = _firstCollection
71+
.AsQueryable()
72+
.Where(c => c.Name.StartsWith("second"))
73+
.Union(_secondCollection.AsQueryable().Where(c => c.Name.StartsWith("another")));
74+
75+
var stages = Translate(_firstCollection, queryable);
76+
AssertStages(stages,
77+
"{ $match : { Name : /^second/s } }",
78+
"{ $unionWith : { coll : 'partners', pipeline : [{ $match : { Name : /^another/s } }] } }");
79+
80+
var results = queryable.ToList();
81+
results.Select(x => x.Id).Should().BeEquivalentTo(new[] { 2, 5 });
82+
}
83+
84+
[Fact]
85+
public void Union_should_support_projection()
86+
{
87+
var queryable = _firstCollection
88+
.AsQueryable()
89+
.Where(c => c.Name.StartsWith("second"))
90+
.Select(c => new ProjectedCompany { Number = c.Id })
91+
.Union(_secondCollection.AsQueryable().Select(c => new ProjectedCompany { Number = c.Id }));
92+
93+
var stages = Translate(_firstCollection, queryable);
94+
AssertStages(stages,
95+
"{ $match : { Name : /^second/s } }",
96+
"{ $project : { Number : '$_id', _id : 0 } }",
97+
"{ $unionWith : { coll : 'partners', pipeline : [{ $project : { Number : '$_id', _id : 0 } }] } }");
98+
99+
var results = queryable.ToList();
100+
results.Select(x => x.Number).Should().BeEquivalentTo(new[] { 2, 4, 5 });
101+
}
102+
103+
[Fact]
104+
public void Union_should_support_projection_to_anonymous()
105+
{
106+
var queryable = _firstCollection
107+
.AsQueryable()
108+
.Where(c => c.Name.StartsWith("second"))
109+
.Select(c => new { Number = c.Id })
110+
.Union(_secondCollection.AsQueryable().Select(c => new { Number = c.Id }));
111+
112+
var stages = Translate(_firstCollection, queryable);
113+
AssertStages(stages,
114+
"{ $match : { Name : /^second/s } }",
115+
"{ $project : { Number : '$_id', _id : 0 } }",
116+
"{ $unionWith : { coll : 'partners', pipeline : [{ $project : { Number : '$_id', _id : 0 } }] } }");
117+
118+
var results = queryable.ToList();
119+
results.Select(x => x.Number).Should().BeEquivalentTo(new[] { 2, 4, 5 });
120+
}
121+
122+
private IMongoCollection<Company> CreateCollection(string collectionName, params Company[] data)
123+
{
124+
var collection = GetCollection<Company>(collectionName);
125+
CreateCollection(collection, data);
126+
127+
return collection;
128+
}
129+
130+
public class Company
131+
{
132+
public int Id { get; set; }
133+
public string Name { get; set; }
134+
}
135+
136+
public class ProjectedCompany
137+
{
138+
public int Number { get; set; }
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)