Skip to content

Commit 3919516

Browse files
CSHARP-2624: Add $replaceWith stage.
1 parent fad2887 commit 3919516

11 files changed

+223
-4
lines changed

src/MongoDB.Driver/AggregateFluent.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ public override IAggregateFluent<TNewResult> ReplaceRoot<TNewResult>(AggregateEx
187187
return WithPipeline(_pipeline.ReplaceRoot(newRoot));
188188
}
189189

190+
public override IAggregateFluent<TNewResult> ReplaceWith<TNewResult>(AggregateExpressionDefinition<TResult, TNewResult> newRoot)
191+
{
192+
return WithPipeline(_pipeline.ReplaceWith(newRoot));
193+
}
194+
190195
public override IAggregateFluent<TResult> Skip(int skip)
191196
{
192197
return WithPipeline(_pipeline.Skip(skip));

src/MongoDB.Driver/AggregateFluentBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ public virtual IAggregateFluent<TNewResult> ReplaceRoot<TNewResult>(AggregateExp
167167
throw new NotImplementedException();
168168
}
169169

170+
/// <inheritdoc />
171+
public virtual IAggregateFluent<TNewResult> ReplaceWith<TNewResult>(AggregateExpressionDefinition<TResult, TNewResult> newRoot)
172+
{
173+
throw new NotImplementedException();
174+
}
175+
170176
/// <inheritdoc />
171177
public abstract IAggregateFluent<TResult> Skip(int skip);
172178

src/MongoDB.Driver/IAggregateFluent.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,14 @@ IAggregateFluent<TNewResult> Lookup<TForeignDocument, TAsElement, TAs, TNewResul
276276
/// <returns>The fluent aggregate interface.</returns>
277277
IAggregateFluent<TNewResult> ReplaceRoot<TNewResult>(AggregateExpressionDefinition<TResult, TNewResult> newRoot);
278278

279+
/// <summary>
280+
/// Appends a $replaceWith stage to the pipeline.
281+
/// </summary>
282+
/// <typeparam name="TNewResult">The type of the new result.</typeparam>
283+
/// <param name="newRoot">The new root.</param>
284+
/// <returns>The fluent aggregate interface.</returns>
285+
IAggregateFluent<TNewResult> ReplaceWith<TNewResult>(AggregateExpressionDefinition<TResult, TNewResult> newRoot);
286+
279287
/// <summary>
280288
/// Appends a skip stage to the pipeline.
281289
/// </summary>

src/MongoDB.Driver/IAggregateFluentExtensions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,24 @@ public static IAggregateFluent<TNewResult> ReplaceRoot<TResult, TNewResult>(
493493
return aggregate.AppendStage(PipelineStageDefinitionBuilder.ReplaceRoot(newRoot));
494494
}
495495

496+
/// <summary>
497+
/// Appends a $replaceWith stage to the pipeline.
498+
/// </summary>
499+
/// <typeparam name="TResult">The type of the result.</typeparam>
500+
/// <typeparam name="TNewResult">The type of the new result.</typeparam>
501+
/// <param name="aggregate">The aggregate.</param>
502+
/// <param name="newRoot">The new root.</param>
503+
/// <returns>
504+
/// The fluent aggregate interface.
505+
/// </returns>
506+
public static IAggregateFluent<TNewResult> ReplaceWith<TResult, TNewResult>(
507+
this IAggregateFluent<TResult> aggregate,
508+
Expression<Func<TResult, TNewResult>> newRoot)
509+
{
510+
Ensure.IsNotNull(aggregate, nameof(aggregate));
511+
return aggregate.AppendStage(PipelineStageDefinitionBuilder.ReplaceWith(newRoot));
512+
}
513+
496514
/// <summary>
497515
/// Appends an ascending sort stage to the pipeline.
498516
/// </summary>

src/MongoDB.Driver/PipelineDefinitionBuilder.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,46 @@ public static PipelineDefinition<TInput, TOutput> ReplaceRoot<TInput, TIntermedi
946946
return pipeline.AppendStage(PipelineStageDefinitionBuilder.ReplaceRoot(newRoot, translationOptions));
947947
}
948948

949+
/// <summary>
950+
/// Appends a $replaceWith stage to the pipeline.
951+
/// </summary>
952+
/// <typeparam name="TInput">The type of the input documents.</typeparam>
953+
/// <typeparam name="TIntermediate">The type of the intermediate documents.</typeparam>
954+
/// <typeparam name="TOutput">The type of the output documents.</typeparam>
955+
/// <param name="pipeline">The pipeline.</param>
956+
/// <param name="newRoot">The new root.</param>
957+
/// <returns>
958+
/// A new pipeline with an additional stage.
959+
/// </returns>
960+
public static PipelineDefinition<TInput, TOutput> ReplaceWith<TInput, TIntermediate, TOutput>(
961+
this PipelineDefinition<TInput, TIntermediate> pipeline,
962+
AggregateExpressionDefinition<TIntermediate, TOutput> newRoot)
963+
{
964+
Ensure.IsNotNull(pipeline, nameof(pipeline));
965+
return pipeline.AppendStage(PipelineStageDefinitionBuilder.ReplaceWith(newRoot));
966+
}
967+
968+
/// <summary>
969+
/// Appends a $replaceWith stage to the pipeline.
970+
/// </summary>
971+
/// <typeparam name="TInput">The type of the input documents.</typeparam>
972+
/// <typeparam name="TIntermediate">The type of the intermediate documents.</typeparam>
973+
/// <typeparam name="TOutput">The type of the output documents.</typeparam>
974+
/// <param name="pipeline">The pipeline.</param>
975+
/// <param name="newRoot">The new root.</param>
976+
/// <param name="translationOptions">The translation options.</param>
977+
/// <returns>
978+
/// The fluent aggregate interface.
979+
/// </returns>
980+
public static PipelineDefinition<TInput, TOutput> ReplaceWith<TInput, TIntermediate, TOutput>(
981+
this PipelineDefinition<TInput, TIntermediate> pipeline,
982+
Expression<Func<TIntermediate, TOutput>> newRoot,
983+
ExpressionTranslationOptions translationOptions = null)
984+
{
985+
Ensure.IsNotNull(pipeline, nameof(pipeline));
986+
return pipeline.AppendStage(PipelineStageDefinitionBuilder.ReplaceWith(newRoot, translationOptions));
987+
}
988+
949989
/// <summary>
950990
/// Appends a $skip stage to the pipeline.
951991
/// </summary>

src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,47 @@ public static PipelineStageDefinition<TInput, TOutput> ReplaceRoot<TInput, TOutp
10771077
return ReplaceRoot(new ExpressionAggregateExpressionDefinition<TInput, TOutput>(newRoot, translationOptions));
10781078
}
10791079

1080+
/// <summary>
1081+
/// Creates a $replaceWith stage.
1082+
/// </summary>
1083+
/// <typeparam name="TInput">The type of the input documents.</typeparam>
1084+
/// <typeparam name="TOutput">The type of the output documents.</typeparam>
1085+
/// <param name="newRoot">The new root.</param>
1086+
/// <returns>The stage.</returns>
1087+
public static PipelineStageDefinition<TInput, TOutput> ReplaceWith<TInput, TOutput>(
1088+
AggregateExpressionDefinition<TInput, TOutput> newRoot)
1089+
{
1090+
Ensure.IsNotNull(newRoot, nameof(newRoot));
1091+
1092+
const string operatorName = "$replaceWith";
1093+
var stage = new DelegatedPipelineStageDefinition<TInput, TOutput>(
1094+
operatorName,
1095+
(s, sr) =>
1096+
{
1097+
var document = new BsonDocument(operatorName, newRoot.Render(s, sr));
1098+
var outputSerializer = sr.GetSerializer<TOutput>();
1099+
return new RenderedPipelineStageDefinition<TOutput>(operatorName, document, outputSerializer);
1100+
});
1101+
1102+
return stage;
1103+
}
1104+
1105+
/// <summary>
1106+
/// Creates a $replaceWith stage.
1107+
/// </summary>
1108+
/// <typeparam name="TInput">The type of the input documents.</typeparam>
1109+
/// <typeparam name="TOutput">The type of the output documents.</typeparam>
1110+
/// <param name="newRoot">The new root.</param>
1111+
/// <param name="translationOptions">The translation options.</param>
1112+
/// <returns>The stage.</returns>
1113+
public static PipelineStageDefinition<TInput, TOutput> ReplaceWith<TInput, TOutput>(
1114+
Expression<Func<TInput, TOutput>> newRoot,
1115+
ExpressionTranslationOptions translationOptions = null)
1116+
{
1117+
Ensure.IsNotNull(newRoot, nameof(newRoot));
1118+
return ReplaceWith(new ExpressionAggregateExpressionDefinition<TInput, TOutput>(newRoot, translationOptions));
1119+
}
1120+
10801121
/// <summary>
10811122
/// Creates a $skip stage.
10821123
/// </summary>

src/MongoDB.Driver/UpdateDefinitionBuilder.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,16 @@ public UpdateDefinition<TDocument> Mul<TField>(Expression<Func<TDocument, TField
10521052
return Mul(new ExpressionFieldDefinition<TDocument, TField>(field), value);
10531053
}
10541054

1055+
/// <summary>
1056+
/// Creates an update pipeline.
1057+
/// </summary>
1058+
/// <param name="pipeline">The pipeline.</param>
1059+
/// <returns>An update pipeline.</returns>
1060+
public UpdateDefinition<TDocument> Pipeline(PipelineDefinition<TDocument, TDocument> pipeline)
1061+
{
1062+
return new PipelineUpdateDefinition<TDocument>(pipeline);
1063+
}
1064+
10551065
/// <summary>
10561066
/// Creates a pop operator.
10571067
/// </summary>

tests/MongoDB.Driver.Tests/AggregateFluentTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,47 @@ public void ReplaceRoot_should_add_the_expected_stage(
628628
}
629629
}
630630

631+
[Theory]
632+
[ParameterAttributeData]
633+
public void ReplaceWith_should_add_the_expected_stage([Values(false, true)] bool async)
634+
{
635+
var subject = CreateSubject();
636+
637+
var result = subject.ReplaceWith<BsonDocument>("$X");
638+
639+
Predicate<PipelineDefinition<C, BsonDocument>> isExpectedPipeline = pipeline =>
640+
{
641+
var renderedPipeline = RenderPipeline(pipeline);
642+
return
643+
renderedPipeline.Documents.Count == 1 &&
644+
renderedPipeline.Documents[0] == BsonDocument.Parse("{ $replaceWith : '$X' }") &&
645+
renderedPipeline.OutputSerializer.ValueType == typeof(BsonDocument);
646+
};
647+
648+
if (async)
649+
{
650+
result.ToCursorAsync().GetAwaiter().GetResult();
651+
652+
_mockCollection.Verify(
653+
c => c.AggregateAsync<BsonDocument>(
654+
It.Is<PipelineDefinition<C, BsonDocument>>(pipeline => isExpectedPipeline(pipeline)),
655+
It.IsAny<AggregateOptions>(),
656+
CancellationToken.None),
657+
Times.Once);
658+
}
659+
else
660+
{
661+
result.ToCursor();
662+
663+
_mockCollection.Verify(
664+
c => c.Aggregate<BsonDocument>(
665+
It.Is<PipelineDefinition<C, BsonDocument>>(pipeline => isExpectedPipeline(pipeline)),
666+
It.IsAny<AggregateOptions>(),
667+
CancellationToken.None),
668+
Times.Once);
669+
}
670+
}
671+
631672
[Theory]
632673
[ParameterAttributeData]
633674
public void SortByCount_should_add_the_expected_stage(

tests/MongoDB.Driver.Tests/IAggregateFluentExtensionsTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,28 @@ public void ReplaceRoot_should_generate_the_correct_stage_with_anonymous_class()
288288
AssertLast(subject, expectedStage);
289289
}
290290

291+
[Fact]
292+
public void ReplaceWith_should_generate_the_correct_stage()
293+
{
294+
var subject = CreateSubject()
295+
.ReplaceWith(x => x.PhoneNumber);
296+
297+
var expectedStage = BsonDocument.Parse("{ $replaceWith : '$PhoneNumber' }");
298+
299+
AssertLast(subject, expectedStage);
300+
}
301+
302+
[Fact]
303+
public void ReplaceWith_should_generate_the_correct_stage_with_anonymous_class()
304+
{
305+
var subject = CreateSubject()
306+
.ReplaceWith(x => new { Name = x.FirstName + " " + x.LastName });
307+
308+
var expectedStage = BsonDocument.Parse("{ $replaceWith : { Name : { $concat : [ '$FirstName', ' ', '$LastName' ] } } }");
309+
310+
AssertLast(subject, expectedStage);
311+
}
312+
291313
[Theory]
292314
[ParameterAttributeData]
293315
public void Single_should_add_limit_and_call_ToCursor(

tests/MongoDB.Driver.Tests/PipelineUpdateDefinitionTests.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,19 @@ public void PipelineUpdateDefinition_should_work_with_pipeline_builder()
4040
result.Should().Be("[{ \"$addFields\" : { \"x\" : 2 } }]");
4141
}
4242

43+
[Fact]
44+
public void PipelineUpdateDefinition_should_work_with_pipeline_builder_and_replaceWith()
45+
{
46+
var pipeline = new EmptyPipelineDefinition<BsonDocument>()
47+
.ReplaceWith((AggregateExpressionDefinition<BsonDocument, BsonDocument>)"{ _id : \"$_id\", s : { $sum : [\"$X\", \"$Y\"] } }");
48+
var subject = CreateSubject(pipeline);
49+
var result = subject.ToString();
50+
result.Should().Be("[{ \"$replaceWith\" : { \"_id\" : \"$_id\", \"s\" : { \"$sum\" : [\"$X\", \"$Y\"] } } }]");
51+
}
52+
4353
private PipelineUpdateDefinition<BsonDocument> CreateSubject(params string[] stages)
4454
{
45-
return CreateSubject(PipelineDefinition<BsonDocument,BsonDocument>.Create(stages));
55+
return CreateSubject(PipelineDefinition<BsonDocument, BsonDocument>.Create(stages));
4656
}
4757

4858
private PipelineUpdateDefinition<BsonDocument> CreateSubject(PipelineDefinition<BsonDocument, BsonDocument> pipeline)

tests/MongoDB.Driver.Tests/UpdateDefinitionBuilderTests.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,16 @@ public void Mul_Typed()
329329
Assert(subject.Mul("Age", 2), "{$mul: {age: 2}}");
330330
}
331331

332+
[Fact]
333+
public void Pipeline()
334+
{
335+
var subject = CreateSubject<BsonDocument>();
336+
var pipeline = new EmptyPipelineDefinition<BsonDocument>()
337+
.AppendStage<BsonDocument, BsonDocument, BsonDocument>("{ $addFields : { x : 2 } }");
338+
339+
Assert(subject.Pipeline(pipeline), new[] { "{ \"$addFields\" : { \"x\" : 2 } }" });
340+
}
341+
332342
[Fact]
333343
public void PopFirst()
334344
{
@@ -639,11 +649,19 @@ public void Unset_Typed()
639649

640650
private void Assert<TDocument>(UpdateDefinition<TDocument> update, BsonDocument expected)
641651
{
642-
var renderedUpdate = Render(update);
652+
var renderedUpdate = Render(update).AsBsonDocument;
643653

644654
renderedUpdate.Should().Be(expected);
645655
}
646656

657+
private void Assert<TDocument>(UpdateDefinition<TDocument> update, string[] expectedArrayItems)
658+
{
659+
var renderedUpdate = Render(update).AsBsonArray;
660+
661+
var bsonArray = new BsonArray(expectedArrayItems.Select(BsonDocument.Parse));
662+
renderedUpdate.Should().Be(bsonArray);
663+
}
664+
647665
private void Assert<TDocument>(UpdateDefinition<TDocument> update, string expected)
648666
{
649667
Assert(update, BsonDocument.Parse(expected));
@@ -661,10 +679,10 @@ private UpdateDefinitionBuilder<TDocument> CreateSubject<TDocument>()
661679
return new UpdateDefinitionBuilder<TDocument>();
662680
}
663681

664-
private BsonDocument Render<TDocument>(UpdateDefinition<TDocument> update)
682+
private BsonValue Render<TDocument>(UpdateDefinition<TDocument> update)
665683
{
666684
var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer<TDocument>();
667-
return update.Render(documentSerializer, BsonSerializer.SerializerRegistry).AsBsonDocument;
685+
return update.Render(documentSerializer, BsonSerializer.SerializerRegistry);
668686
}
669687

670688
private class Person

0 commit comments

Comments
 (0)