Skip to content

Commit 6b29843

Browse files
committed
CSHARP-4234: Add AppendStage extension method to IMongoQueryable.
1 parent bbeeb21 commit 6b29843

File tree

9 files changed

+227
-0
lines changed

9 files changed

+227
-0
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstNodeType.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ internal enum AstNodeType
143143
UnaryExpression,
144144
UnaryWindowExpression,
145145
UnionWithStage,
146+
UniversalStage,
146147
UnsetStage,
147148
UnwindStage,
148149
VarBinding,

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Stages/AstStage.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ public static AstStage UnionWith(string collection, AstPipeline pipeline)
230230
return new AstUnionWithStage(collection, pipeline);
231231
}
232232

233+
public static AstStage Universal(BsonDocument stage)
234+
{
235+
return new AstUniversalStage(stage);
236+
}
237+
233238
public static AstStage Unset(IEnumerable<string> fields)
234239
{
235240
return new AstUnsetStage(fields);
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 MongoDB.Bson;
17+
using MongoDB.Driver.Core.Misc;
18+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors;
19+
20+
namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Stages
21+
{
22+
internal sealed class AstUniversalStage : AstStage
23+
{
24+
private readonly BsonDocument _stage;
25+
26+
public AstUniversalStage(BsonDocument stage)
27+
{
28+
_stage = Ensure.IsNotNull(stage, nameof(stage));
29+
}
30+
31+
public override AstNodeType NodeType => AstNodeType.UniversalStage;
32+
public BsonDocument Stage => _stage;
33+
34+
public override AstNode Accept(AstNodeVisitor visitor)
35+
{
36+
return visitor.VisitUniversalStage(this);
37+
}
38+
39+
public override BsonValue Render()
40+
{
41+
return _stage;
42+
}
43+
44+
public AstUniversalStage Update(BsonDocument stage)
45+
{
46+
if (stage == _stage)
47+
{
48+
return this;
49+
}
50+
51+
return new AstUniversalStage(stage);
52+
}
53+
}
54+
}

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Visitors/AstNodeVisitor.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,11 @@ public virtual AstNode VisitUnionWithStage(AstUnionWithStage node)
749749
return node.Update(VisitAndConvert(node.Pipeline));
750750
}
751751

752+
public virtual AstNode VisitUniversalStage(AstUniversalStage node)
753+
{
754+
return node.Update(node.Stage);
755+
}
756+
752757
public virtual AstNode VisitUnsetStage(AstUnsetStage node)
753758
{
754759
return node;

src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MongoQueryableMethod.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
using System.Linq.Expressions;
1818
using System.Reflection;
1919
using System.Threading;
20+
using MongoDB.Bson.Serialization;
2021

2122
namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection
2223
{
@@ -25,6 +26,7 @@ internal static class MongoQueryableMethod
2526
// private static fields
2627
private static readonly MethodInfo __anyAsync;
2728
private static readonly MethodInfo __anyWithPredicateAsync;
29+
private static readonly MethodInfo __appendStage;
2830
private static readonly MethodInfo __averageDecimalAsync;
2931
private static readonly MethodInfo __averageDecimalWithSelectorAsync;
3032
private static readonly MethodInfo __averageDoubleAsync;
@@ -169,6 +171,7 @@ static MongoQueryableMethod()
169171
{
170172
__anyAsync = ReflectionInfo.Method((IMongoQueryable<object> source, CancellationToken cancellationToken) => source.AnyAsync(cancellationToken));
171173
__anyWithPredicateAsync = ReflectionInfo.Method((IMongoQueryable<object> source, Expression<Func<object, bool>> predicate, CancellationToken cancellationToken) => source.AnyAsync(predicate, cancellationToken));
174+
__appendStage = ReflectionInfo.Method((IMongoQueryable<object> source, PipelineStageDefinition<object, object> stage, IBsonSerializer<object> resultSerializer) => source.AppendStage(stage, resultSerializer));
172175
__averageDecimalAsync = ReflectionInfo.Method((IMongoQueryable<decimal> source, CancellationToken cancellationToken) => source.AverageAsync(cancellationToken));
173176
__averageDecimalWithSelectorAsync = ReflectionInfo.Method((IMongoQueryable<object> source, Expression<Func<object, decimal>> selector, CancellationToken cancellationToken) => source.AverageAsync(selector, cancellationToken));
174177
__averageDoubleAsync = ReflectionInfo.Method((IMongoQueryable<double> source, CancellationToken cancellationToken) => source.AverageAsync(cancellationToken));
@@ -312,6 +315,7 @@ static MongoQueryableMethod()
312315
// public properties
313316
public static MethodInfo AnyAsync => __anyAsync;
314317
public static MethodInfo AnyWithPredicateAsync => __anyWithPredicateAsync;
318+
public static MethodInfo AppendStage => __appendStage;
315319
public static MethodInfo AverageDecimalAsync => __averageDecimalAsync;
316320
public static MethodInfo AverageDecimalWithSelectorAsync => __averageDecimalWithSelectorAsync;
317321
public static MethodInfo AverageDoubleAsync => __averageDoubleAsync;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.Bson.Serialization;
18+
using MongoDB.Driver.Linq.Linq3Implementation.Ast;
19+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Stages;
20+
using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods;
21+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
22+
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
23+
24+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToPipelineTranslators
25+
{
26+
internal static class AppendStageMethodToPipelineTranslator
27+
{
28+
// public static methods
29+
public static AstPipeline Translate(TranslationContext context, MethodCallExpression expression)
30+
{
31+
var method = expression.Method;
32+
var arguments = expression.Arguments;
33+
34+
if (method.Is(MongoQueryableMethod.AppendStage))
35+
{
36+
var sourceExpression = ConvertHelper.RemoveConvertToMongoQueryable(arguments[0]);
37+
var pipeline = ExpressionToPipelineTranslator.Translate(context, sourceExpression);
38+
var sourceSerializer = pipeline.OutputSerializer;
39+
40+
var stageExpression = arguments[1];
41+
var renderedStage = TranslateStage(expression, stageExpression, sourceSerializer, BsonSerializer.SerializerRegistry);
42+
var stage = AstStage.Universal(renderedStage.Document);
43+
44+
var resultSerializerExpression = arguments[2];
45+
var resultSerializer = resultSerializerExpression.GetConstantValue<IBsonSerializer>(expression);
46+
var outputSerializer = resultSerializer ?? renderedStage.OutputSerializer;
47+
48+
pipeline = pipeline.AddStages(outputSerializer, stage);
49+
return pipeline;
50+
}
51+
52+
throw new ExpressionNotSupportedException(expression);
53+
}
54+
55+
private static IRenderedPipelineStageDefinition TranslateStage(
56+
Expression expression,
57+
Expression stageExpression,
58+
IBsonSerializer inputSerializer,
59+
IBsonSerializerRegistry serializerRegistry)
60+
{
61+
var stageDefinition = stageExpression.GetConstantValue<IPipelineStageDefinition>(stageExpression);
62+
return stageDefinition.Render(inputSerializer, serializerRegistry, LinqProvider.V3);
63+
}
64+
}
65+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public static AstPipeline Translate(TranslationContext context, Expression expre
3434
var methodCallExpression = (MethodCallExpression)expression;
3535
switch (methodCallExpression.Method.Name)
3636
{
37+
case "AppendStage":
38+
return AppendStageMethodToPipelineTranslator.Translate(context, methodCallExpression);
3739
case "Densify":
3840
return DensifyMethodToPipelineTranslator.Translate(context, methodCallExpression);
3941
case "Distinct":

src/MongoDB.Driver/Linq/MongoQueryable.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using System.Reflection;
2121
using System.Threading;
2222
using System.Threading.Tasks;
23+
using MongoDB.Bson.Serialization;
2324

2425
namespace MongoDB.Driver.Linq
2526
{
@@ -66,6 +67,29 @@ public static class MongoQueryable
6667
cancellationToken);
6768
}
6869

70+
/// <summary>
71+
/// Appends an arbitrary stage to the LINQ pipeline.
72+
/// </summary>
73+
/// <typeparam name="TSource">The type of the elements of <paramref name="source" />.</typeparam>
74+
/// <typeparam name="TResult">The type of the result values returned by the appended stage.</typeparam>
75+
/// <param name="source">A sequence of values.</param>
76+
/// <param name="stage">The stage to append.</param>
77+
/// <param name="resultSerializer">The result serializer.</param>
78+
/// <returns>The queryable with a new stage appended.</returns>
79+
public static IMongoQueryable<TResult> AppendStage<TSource, TResult>(
80+
this IMongoQueryable<TSource> source,
81+
PipelineStageDefinition<TSource, TResult> stage,
82+
IBsonSerializer<TResult> resultSerializer = null)
83+
{
84+
return (IMongoQueryable<TResult>)source.Provider.CreateQuery<TResult>(
85+
Expression.Call(
86+
null,
87+
GetMethodInfo(AppendStage, source, stage, resultSerializer),
88+
Expression.Convert(source.Expression, typeof(IMongoQueryable<TSource>)),
89+
Expression.Constant(stage),
90+
Expression.Constant(resultSerializer)));
91+
}
92+
6993
/// <summary>
7094
/// Computes the average of a sequence of <see cref="Decimal"/> values.
7195
/// </summary>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 MongoDB.Bson;
19+
using MongoDB.Driver.Linq;
20+
using Xunit;
21+
22+
namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Jira
23+
{
24+
public class CSharp4234Tests : Linq3IntegrationTest
25+
{
26+
[Fact]
27+
public void AppendStage_should_work()
28+
{
29+
var collection = CreateProductsCollection();
30+
var textStage = "{ $match : { $text : { $search : 'apples' } } }";
31+
var resultSerializer = collection.DocumentSerializer;
32+
33+
var queryable = collection
34+
.AsQueryable()
35+
.AppendStage(BsonDocument.Parse(textStage), resultSerializer);
36+
37+
var stages = Translate(collection, queryable);
38+
AssertStages(stages, textStage);
39+
40+
var results = queryable.ToList();
41+
results.Select(r => r.Id).Should().Equal(1);
42+
}
43+
44+
private IMongoCollection<Product> CreateProductsCollection()
45+
{
46+
var collection = GetCollection<Product>("products");
47+
var database = collection.Database;
48+
49+
CreateCollection(
50+
collection,
51+
new Product { Id = 1, Name = "Apples" },
52+
new Product { Id = 2, Name = "Oranges" });
53+
54+
var keys = Builders<Product>.IndexKeys.Text(p => p.Name);
55+
var model = new CreateIndexModel<Product>(keys);
56+
collection.Indexes.CreateOne(model);
57+
58+
return collection;
59+
}
60+
61+
public class Product
62+
{
63+
public int Id { get; set; }
64+
public string Name { get; set; }
65+
}
66+
}
67+
}

0 commit comments

Comments
 (0)