Skip to content

Commit 4e06ffa

Browse files
committed
CSHARP-1715: Added support for $facet aggregation stage.
1 parent b07f674 commit 4e06ffa

13 files changed

+1156
-25
lines changed

src/MongoDB.Driver.Core/Core/Misc/Feature.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class Feature
2929
private static readonly Feature __aggregateCountStage = new Feature("AggregateCountStage", new SemanticVersion(3, 3, 11));
3030
private static readonly Feature __aggregateCursorResult = new Feature("AggregateCursorResult", new SemanticVersion(2, 6, 0));
3131
private static readonly Feature __aggregateExplain = new Feature("AggregateExplain", new SemanticVersion(2, 6, 0));
32+
private static readonly Feature __aggregateFacetStage = new Feature("AggregateFacetStage", new SemanticVersion(3, 4, 0, "rc0"));
3233
private static readonly Feature __aggregateOut = new Feature("Aggregate", new SemanticVersion(2, 6, 0));
3334
private static readonly Feature __bypassDocumentValidation = new Feature("BypassDocumentValidation", new SemanticVersion(3, 2, 0));
3435
private static readonly CollationFeature __collation = new CollationFeature("Collation", new SemanticVersion(3, 3, 11));
@@ -82,6 +83,11 @@ public class Feature
8283
/// </summary>
8384
public static Feature AggregateExplain => __aggregateExplain;
8485

86+
/// <summary>
87+
/// Gets the aggregate $facet stage feature.
88+
/// </summary>
89+
public static Feature AggregateFacetStage => __aggregateFacetStage;
90+
8591
/// <summary>
8692
/// Gets the aggregate out feature.
8793
/// </summary>

src/MongoDB.Driver/AggregateFacet.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* Copyright 2016 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 MongoDB.Bson;
18+
using MongoDB.Bson.Serialization;
19+
using MongoDB.Driver.Core.Misc;
20+
21+
namespace MongoDB.Driver
22+
{
23+
/// <summary>
24+
/// Represents static methods for creating facets.
25+
/// </summary>
26+
public static class AggregateFacet
27+
{
28+
/// <summary>
29+
/// Creates a new instance of the <see cref="AggregateFacet{TInput, TOutput}" /> class.
30+
/// </summary>
31+
/// <typeparam name="TInput">The type of the input documents.</typeparam>
32+
/// <typeparam name="TOutput">The type of the output documents.</typeparam>
33+
/// <param name="name">The facet name.</param>
34+
/// <param name="pipeline">The facet pipeline.</param>
35+
/// <returns>
36+
/// A new instance of the <see cref="AggregateFacet{TInput, TOutput}" /> class
37+
/// </returns>
38+
public static AggregateFacet<TInput, TOutput> Create<TInput, TOutput>(string name, PipelineDefinition<TInput, TOutput> pipeline)
39+
{
40+
return new AggregateFacet<TInput, TOutput>(name, pipeline);
41+
}
42+
}
43+
44+
/// <summary>
45+
/// Represents a facet to be passed to the Facet method.
46+
/// </summary>
47+
/// <typeparam name="TInput">The type of the input documents.</typeparam>
48+
public abstract class AggregateFacet<TInput>
49+
{
50+
/// <summary>
51+
/// Initializes a new instance of the <see cref="AggregateFacet{TInput}"/> class.
52+
/// </summary>
53+
/// <param name="name">The facet name.</param>
54+
protected AggregateFacet(string name)
55+
{
56+
Name = Ensure.IsNotNull(name, nameof(name));
57+
}
58+
59+
/// <summary>
60+
/// Gets the facet name.
61+
/// </summary>
62+
public string Name { get; private set; }
63+
64+
/// <summary>
65+
/// Gets the output serializer.
66+
/// </summary>
67+
public abstract IBsonSerializer OutputSerializer { get; }
68+
69+
/// <summary>
70+
/// Gets the type of the output documents.
71+
/// </summary>
72+
public abstract Type OutputType { get; }
73+
74+
/// <summary>
75+
/// Renders the facet pipeline.
76+
/// </summary>
77+
/// <param name="inputSerializer">The input serializer.</param>
78+
/// <param name="serializerRegistry">The serializer registry.</param>
79+
/// <returns>The rendered pipeline.</returns>
80+
public abstract BsonArray RenderPipeline(IBsonSerializer<TInput> inputSerializer, IBsonSerializerRegistry serializerRegistry);
81+
}
82+
83+
/// <summary>
84+
/// Represents a facet to be passed to the Facet method.
85+
/// </summary>
86+
/// <typeparam name="TInput">The type of the input documents.</typeparam>
87+
/// <typeparam name="TOutput">The type of the otuput documents.</typeparam>
88+
public class AggregateFacet<TInput, TOutput> : AggregateFacet<TInput>
89+
{
90+
/// <summary>
91+
/// Initializes a new instance of the <see cref="AggregateFacet{TInput, TOutput}"/> class.
92+
/// </summary>
93+
/// <param name="name">The facet name.</param>
94+
/// <param name="pipeline">The facet pipeline.</param>
95+
public AggregateFacet(string name, PipelineDefinition<TInput, TOutput> pipeline)
96+
: base(name)
97+
{
98+
Pipeline = Ensure.IsNotNull(pipeline, nameof(pipeline));
99+
}
100+
101+
/// <inheritdoc/>
102+
public override IBsonSerializer OutputSerializer => Pipeline.OutputSerializer;
103+
104+
/// <inheritdoc/>
105+
public override Type OutputType => typeof(TOutput);
106+
107+
/// <summary>
108+
/// Gets the facet pipeline.
109+
/// </summary>
110+
public PipelineDefinition<TInput, TOutput> Pipeline { get; private set; }
111+
112+
/// <inheritdoc/>
113+
public override BsonArray RenderPipeline(IBsonSerializer<TInput> inputSerializer, IBsonSerializerRegistry serializerRegistry)
114+
{
115+
var renderedPipeline = Pipeline.Render(inputSerializer, serializerRegistry);
116+
return new BsonArray(renderedPipeline.Documents);
117+
}
118+
}
119+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* Copyright 2016 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.Serialization;
17+
18+
namespace MongoDB.Driver
19+
{
20+
/// <summary>
21+
/// Options for the aggregate $facet stage.
22+
/// </summary>
23+
/// <typeparam name="TNewResult">The type of the new result.</typeparam>
24+
public sealed class AggregateFacetOptions<TNewResult>
25+
{
26+
private IBsonSerializer<TNewResult> _newResultSerializer;
27+
28+
/// <summary>
29+
/// Gets or sets the new result serializer.
30+
/// </summary>
31+
public IBsonSerializer<TNewResult> NewResultSerializer
32+
{
33+
get { return _newResultSerializer; }
34+
set { _newResultSerializer = value; }
35+
}
36+
}
37+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/* Copyright 2016 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.Collections.Generic;
17+
using System.Linq;
18+
using MongoDB.Driver.Core.Misc;
19+
20+
namespace MongoDB.Driver
21+
{
22+
/// <summary>
23+
/// Represents an abstract AggregateFacetResult with an arbitrary TOutput type.
24+
/// </summary>
25+
public abstract class AggregateFacetResult
26+
{
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="AggregateFacetResult" /> class.
29+
/// </summary>
30+
/// <param name="name">The name of the facet.</param>
31+
internal AggregateFacetResult(string name)
32+
{
33+
Name = Ensure.IsNotNull(name, nameof(name));
34+
}
35+
36+
/// <summary>
37+
/// Gets the name of the facet.
38+
/// </summary>
39+
public string Name { get; private set; }
40+
41+
/// <summary>
42+
/// Gets the output of the facet.
43+
/// </summary>
44+
public IReadOnlyList<TOutput> Output<TOutput>()
45+
{
46+
return ((AggregateFacetResult<TOutput>)this).Output;
47+
}
48+
}
49+
50+
/// <summary>
51+
/// Represents the result of a single facet.
52+
/// </summary>
53+
/// <typeparam name="TOutput">The type of the output.</typeparam>
54+
public sealed class AggregateFacetResult<TOutput> : AggregateFacetResult
55+
{
56+
/// <summary>
57+
/// Initializes a new instance of the <see cref="AggregateFacetResult{TOutput}"/> class.
58+
/// </summary>
59+
/// <param name="name">The name.</param>
60+
/// <param name="output">The output.</param>
61+
public AggregateFacetResult(string name, IEnumerable<TOutput> output)
62+
: base(name)
63+
{
64+
Ensure.IsNotNull(output, nameof(output));
65+
66+
var readOnlyList = output as IReadOnlyList<TOutput>;
67+
if (readOnlyList != null)
68+
{
69+
Output = readOnlyList;
70+
}
71+
else
72+
{
73+
Output = output.ToArray();
74+
}
75+
}
76+
77+
/// <summary>
78+
/// Gets or sets the output.
79+
/// </summary>
80+
/// <value>
81+
/// The output.
82+
/// </value>
83+
public IReadOnlyList<TOutput> Output { get; set; }
84+
}
85+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/* Copyright 2016 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 System.Reflection;
20+
using MongoDB.Bson;
21+
using MongoDB.Bson.IO;
22+
using MongoDB.Bson.Serialization;
23+
using MongoDB.Bson.Serialization.Serializers;
24+
using MongoDB.Driver.Core.Misc;
25+
26+
namespace MongoDB.Driver
27+
{
28+
/// <summary>
29+
/// Represents the results of a $facet stage with an arbitrary number of facets.
30+
/// </summary>
31+
public class AggregateFacetResults
32+
{
33+
/// <summary>
34+
/// Initializes a new instance of the <see cref="AggregateFacetResults"/> class.
35+
/// </summary>
36+
/// <param name="facets">The facets.</param>
37+
public AggregateFacetResults(AggregateFacetResult[] facets)
38+
{
39+
Facets = Ensure.IsNotNull(facets, nameof(facets));
40+
}
41+
42+
/// <summary>
43+
/// Gets the facets.
44+
/// </summary>
45+
public IReadOnlyList<AggregateFacetResult> Facets { get; private set; }
46+
}
47+
48+
internal class AggregateFacetResultsSerializer : SerializerBase<AggregateFacetResults>
49+
{
50+
private readonly string[] _names;
51+
private readonly IBsonSerializer[] _serializers;
52+
53+
public AggregateFacetResultsSerializer(IEnumerable<string> names, IEnumerable<IBsonSerializer> serializers)
54+
{
55+
_names = names.ToArray();
56+
_serializers = serializers.ToArray();
57+
}
58+
59+
public override AggregateFacetResults Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
60+
{
61+
var facets = new AggregateFacetResult[_names.Length];
62+
63+
var reader = context.Reader;
64+
reader.ReadStartDocument();
65+
while (reader.ReadBsonType() != 0)
66+
{
67+
var name = reader.ReadName();
68+
var index = Array.IndexOf(_names, name);
69+
if (index != -1)
70+
{
71+
var itemSerializer = _serializers[index];
72+
var itemType = itemSerializer.ValueType;
73+
var itemSerializerType = typeof(IBsonSerializer<>).MakeGenericType(itemType);
74+
var arraySerializerType = typeof(ArraySerializer<>).MakeGenericType(itemType);
75+
var arraySerializerConstructor = arraySerializerType.GetTypeInfo().GetConstructor(new[] { itemSerializerType });
76+
var arraySerializer = (IBsonSerializer)arraySerializerConstructor.Invoke(new object[] { itemSerializer });
77+
var output = (Array)arraySerializer.Deserialize(context);
78+
var facetType = typeof(AggregateFacetResult<>).MakeGenericType(itemType);
79+
var ienumerableItemType = typeof(IEnumerable<>).MakeGenericType(itemType);
80+
var facetConstructor = facetType.GetTypeInfo().GetConstructor(new[] { typeof(string), ienumerableItemType });
81+
var facet = (AggregateFacetResult)facetConstructor.Invoke(new object[] { name, output });
82+
facets[index] = facet;
83+
}
84+
else
85+
{
86+
throw new BsonSerializationException($"Unexpected field name '{name}' in $facet result.");
87+
}
88+
}
89+
reader.ReadEndDocument();
90+
91+
var missingIndex = Array.IndexOf(facets, null);
92+
if (missingIndex != -1)
93+
{
94+
var missingName = _names[missingIndex];
95+
throw new BsonSerializationException($"Field name '{missingName}' in $facet result.");
96+
}
97+
98+
99+
return new AggregateFacetResults(facets);
100+
}
101+
}
102+
}

src/MongoDB.Driver/AggregateFluent.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,30 @@ public override IAggregateFluent<AggregateCountResult> Count()
231231
return AppendStage<AggregateCountResult>(stage);
232232
}
233233

234+
public override IAggregateFluent<TNewResult> Facet<TNewResult>(
235+
IEnumerable<AggregateFacet<TResult>> facets,
236+
AggregateFacetOptions<TNewResult> options = null)
237+
{
238+
const string operatorName = "$facet";
239+
var materializedFacets = facets.ToArray();
240+
var stage = new DelegatedPipelineStageDefinition<TResult, TNewResult>(
241+
operatorName,
242+
(s, sr) =>
243+
{
244+
var facetsDocument = new BsonDocument();
245+
foreach (var facet in materializedFacets)
246+
{
247+
var renderedPipeline = facet.RenderPipeline(s, sr);
248+
facetsDocument.Add(facet.Name, renderedPipeline);
249+
}
250+
var document = new BsonDocument("$facet", facetsDocument);
251+
var resultSerializer = options?.NewResultSerializer ?? sr.GetSerializer<TNewResult>();
252+
return new RenderedPipelineStageDefinition<TNewResult>(operatorName, document, resultSerializer);
253+
});
254+
255+
return AppendStage<TNewResult>(stage);
256+
}
257+
234258
public override IAggregateFluent<TNewResult> Group<TNewResult>(ProjectionDefinition<TResult, TNewResult> group)
235259
{
236260
const string operatorName = "$group";

0 commit comments

Comments
 (0)