Skip to content

Commit 5d20d20

Browse files
committed
CSHARP-1742: Added support for $replaceRoot aggregation stage.
1 parent c6b1f2c commit 5d20d20

File tree

6 files changed

+122
-0
lines changed

6 files changed

+122
-0
lines changed

src/MongoDB.Driver/AggregateFluent.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,21 @@ public override IAggregateFluent<TNewResult> Project<TNewResult>(ProjectionDefin
197197
return AppendStage<TNewResult>(stage);
198198
}
199199

200+
public override IAggregateFluent<TNewResult> ReplaceRoot<TNewResult>(AggregateExpressionDefinition<TResult, TNewResult> newRoot)
201+
{
202+
const string operatorName = "$replaceRoot";
203+
var stage = new DelegatedPipelineStageDefinition<TResult, TNewResult>(
204+
operatorName,
205+
(s, sr) =>
206+
{
207+
var document = new BsonDocument(operatorName, new BsonDocument("newRoot", newRoot.Render(s, sr)));
208+
var outputSerializer = sr.GetSerializer<TNewResult>();
209+
return new RenderedPipelineStageDefinition<TNewResult>(operatorName, document, outputSerializer);
210+
});
211+
212+
return AppendStage(stage);
213+
}
214+
200215
public override IAggregateFluent<TResult> Skip(int skip)
201216
{
202217
return AppendStage<TResult>(new BsonDocument("$skip", skip));

src/MongoDB.Driver/AggregateFluentBase.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ public virtual IAsyncCursor<TResult> Out(string collectionName, CancellationToke
7676
/// <inheritdoc />
7777
public abstract IAggregateFluent<TNewResult> Project<TNewResult>(ProjectionDefinition<TResult, TNewResult> projection);
7878

79+
/// <inheritdoc />
80+
public virtual IAggregateFluent<TNewResult> ReplaceRoot<TNewResult>(AggregateExpressionDefinition<TResult, TNewResult> newRoot)
81+
{
82+
throw new NotImplementedException();
83+
}
84+
7985
/// <inheritdoc />
8086
public abstract IAggregateFluent<TResult> Skip(int skip);
8187

src/MongoDB.Driver/IAggregateFluent.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ public interface IAggregateFluent<TResult> : IAsyncCursorSource<TResult>
133133
/// </returns>
134134
IAggregateFluent<TNewResult> Project<TNewResult>(ProjectionDefinition<TResult, TNewResult> projection);
135135

136+
/// <summary>
137+
/// Appends a $replaceRoot stage to the pipeline.
138+
/// </summary>
139+
/// <typeparam name="TNewResult">The type of the new result.</typeparam>
140+
/// <param name="newRoot">The new root.</param>
141+
/// <returns>The fluent aggregate interface.</returns>
142+
IAggregateFluent<TNewResult> ReplaceRoot<TNewResult>(AggregateExpressionDefinition<TResult, TNewResult> newRoot);
143+
136144
/// <summary>
137145
/// Appends a skip stage to the pipeline.
138146
/// </summary>

src/MongoDB.Driver/IAggregateFluentExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,26 @@ public static IAggregateFluent<TNewResult> Project<TResult, TNewResult>(this IAg
191191
return aggregate.Project<TNewResult>(new ProjectExpressionProjection<TResult, TNewResult>(projection, aggregate.Options.TranslationOptions));
192192
}
193193

194+
/// <summary>
195+
/// Appends a $replaceRoot stage to the pipeline.
196+
/// </summary>
197+
/// <typeparam name="TResult">The type of the result.</typeparam>
198+
/// <typeparam name="TNewResult">The type of the new result.</typeparam>
199+
/// <param name="aggregate">The aggregate.</param>
200+
/// <param name="newRoot">The new root.</param>
201+
/// <returns>
202+
/// The fluent aggregate interface.
203+
/// </returns>
204+
public static IAggregateFluent<TNewResult> ReplaceRoot<TResult, TNewResult>(
205+
this IAggregateFluent<TResult> aggregate,
206+
Expression<Func<TResult, TNewResult>> newRoot)
207+
{
208+
Ensure.IsNotNull(aggregate, nameof(aggregate));
209+
Ensure.IsNotNull(newRoot, nameof(newRoot));
210+
211+
return aggregate.ReplaceRoot<TNewResult>(new ExpressionAggregateExpressionDefinition<TResult, TNewResult>(newRoot, aggregate.Options.TranslationOptions));
212+
}
213+
194214
/// <summary>
195215
/// Appends an ascending sort stage to the pipeline.
196216
/// </summary>

tests/MongoDB.Driver.Tests/AggregateFluentTests.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,50 @@ public void Out_should_add_the_expected_stage_and_call_Aggregate(
240240
}
241241
}
242242

243+
[Theory]
244+
[ParameterAttributeData]
245+
public void ReplaceRoot_should_add_the_expected_stage(
246+
[Values(false, true)]
247+
bool async)
248+
{
249+
var subject = CreateSubject();
250+
251+
var result = subject
252+
.ReplaceRoot<BsonDocument>("$X");
253+
254+
Predicate<PipelineDefinition<C, BsonDocument>> isExpectedPipeline = pipeline =>
255+
{
256+
var renderedPipeline = RenderPipeline(pipeline);
257+
return
258+
renderedPipeline.Documents.Count == 1 &&
259+
renderedPipeline.Documents[0] == BsonDocument.Parse("{ $replaceRoot : { newRoot : '$X' } }") &&
260+
renderedPipeline.OutputSerializer.ValueType == typeof(BsonDocument);
261+
};
262+
263+
if (async)
264+
{
265+
result.ToCursorAsync().GetAwaiter().GetResult();
266+
267+
_mockCollection.Verify(
268+
c => c.AggregateAsync<BsonDocument>(
269+
It.Is<PipelineDefinition<C, BsonDocument>>(pipeline => isExpectedPipeline(pipeline)),
270+
It.IsAny<AggregateOptions>(),
271+
CancellationToken.None),
272+
Times.Once);
273+
}
274+
else
275+
{
276+
result.ToCursor();
277+
278+
_mockCollection.Verify(
279+
c => c.Aggregate<BsonDocument>(
280+
It.Is<PipelineDefinition<C, BsonDocument>>(pipeline => isExpectedPipeline(pipeline)),
281+
It.IsAny<AggregateOptions>(),
282+
CancellationToken.None),
283+
Times.Once);
284+
}
285+
}
286+
243287
[Theory]
244288
[ParameterAttributeData]
245289
public void SortByCount_should_add_the_expected_stage(

tests/MongoDB.Driver.Tests/IAggregateFluentExtensionsTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,28 @@ public void Project_should_generate_the_correct_document_using_expressions()
231231
AssertLast(subject, expectedProject);
232232
}
233233

234+
[Fact]
235+
public void ReplaceRoot_should_generate_the_correct_stage()
236+
{
237+
var subject = CreateSubject()
238+
.ReplaceRoot(x => x.PhoneNumber);
239+
240+
var expectedStage = BsonDocument.Parse("{ $replaceRoot : { newRoot: '$PhoneNumber' } }");
241+
242+
AssertLast(subject, expectedStage);
243+
}
244+
245+
[Fact]
246+
public void ReplaceRoot_should_generate_the_correct_stage_with_anonymous_class()
247+
{
248+
var subject = CreateSubject()
249+
.ReplaceRoot(x => new { Name = x.FirstName + " " + x.LastName });
250+
251+
var expectedStage = BsonDocument.Parse("{ $replaceRoot : { newRoot: { Name : { $concat : [ '$FirstName', ' ', '$LastName' ] } } } }");
252+
253+
AssertLast(subject, expectedStage);
254+
}
255+
234256
[Theory]
235257
[ParameterAttributeData]
236258
public void Single_should_add_limit_and_call_ToCursor(
@@ -579,6 +601,13 @@ public class Person
579601
public string FirstName;
580602
public string LastName;
581603
public int Age;
604+
public PhoneNumber PhoneNumber;
605+
}
606+
607+
public class PhoneNumber
608+
{
609+
public int AreaCode;
610+
public int Number;
582611
}
583612

584613
public class NameMeaning

0 commit comments

Comments
 (0)