Skip to content

Commit bbeeb21

Browse files
committed
CSHARP-3958: Add an expression to sort an array.
1 parent 216e3b7 commit bbeeb21

File tree

11 files changed

+657
-2
lines changed

11 files changed

+657
-2
lines changed

src/MongoDB.Bson/Serialization/CollectionsSerializationProvider.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,21 @@ public override IBsonSerializer GetSerializer(Type type, IBsonSerializerRegistry
7373

7474
if (typeInfo.IsGenericType && !typeInfo.ContainsGenericParameters)
7575
{
76+
var genericTypeDefinition = type.GetGenericTypeDefinition();
77+
7678
Type serializerTypeDefinition;
77-
if (__serializerTypes.TryGetValue(type.GetGenericTypeDefinition(), out serializerTypeDefinition))
79+
if (__serializerTypes.TryGetValue(genericTypeDefinition, out serializerTypeDefinition))
7880
{
7981
return CreateGenericSerializer(serializerTypeDefinition, typeInfo.GetGenericArguments(), serializerRegistry);
8082
}
83+
84+
if (genericTypeDefinition == typeof(IOrderedEnumerable<>))
85+
{
86+
var itemType = type.GetGenericArguments()[0];
87+
var itemSerializer = serializerRegistry.GetSerializer(itemType);
88+
var thenByExceptionMessage = "ThenBy or ThenByDescending are not supported here.";
89+
return IOrderedEnumerableSerializer.Create(itemSerializer, thenByExceptionMessage);
90+
}
8191
}
8292

8393
if (type.IsArray)
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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;
17+
using System.Collections;
18+
using System.Collections.Generic;
19+
using System.Linq;
20+
21+
namespace MongoDB.Bson.Serialization.Serializers
22+
{
23+
/// <summary>
24+
/// Represents a serializer for IOrderedEnumerable<typeparamref name="TItem"/>.
25+
/// </summary>
26+
/// <typeparam name="TItem">The type of the items.</typeparam>
27+
public class IOrderedEnumerableSerializer<TItem> : SerializerBase<IOrderedEnumerable<TItem>>, IBsonArraySerializer
28+
{
29+
// private fields
30+
private readonly IBsonSerializer<TItem> _itemSerializer;
31+
private readonly string _thenByExceptionMessage;
32+
33+
// public constructors
34+
/// <summary>
35+
/// Initializes a new instance of IOrderedEnumerableSerializer.
36+
/// </summary>
37+
/// <param name="itemSerializer">The item serializer.</param>
38+
/// <param name="thenByExceptionMessage">The message to use when throwing an exception because ThenBy is not supported.</param>
39+
public IOrderedEnumerableSerializer(IBsonSerializer<TItem> itemSerializer, string thenByExceptionMessage)
40+
{
41+
_itemSerializer = itemSerializer ?? throw new ArgumentNullException(nameof(itemSerializer));
42+
_thenByExceptionMessage = thenByExceptionMessage ?? throw new ArgumentNullException(nameof(thenByExceptionMessage));
43+
}
44+
45+
// public methods
46+
/// <inheritdoc/>
47+
public override IOrderedEnumerable<TItem> Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
48+
{
49+
var reader = context.Reader;
50+
reader.ReadStartArray();
51+
var list = new List<TItem>();
52+
while (reader.ReadBsonType() != 0)
53+
{
54+
var item = _itemSerializer.Deserialize(context);
55+
list.Add(item);
56+
}
57+
reader.ReadEndArray();
58+
return new OrderedEnumerableListWrapper<TItem>(list, _thenByExceptionMessage);
59+
}
60+
61+
/// <inheritdoc/>
62+
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, IOrderedEnumerable<TItem> value)
63+
{
64+
var writer = context.Writer;
65+
writer.WriteStartArray();
66+
foreach (var item in value)
67+
{
68+
_itemSerializer.Serialize(context, item);
69+
}
70+
writer.WriteEndArray();
71+
}
72+
73+
/// <inheritdoc/>
74+
public bool TryGetItemSerializationInfo(out BsonSerializationInfo serializationInfo)
75+
{
76+
serializationInfo = new BsonSerializationInfo(null, _itemSerializer, typeof(TItem));
77+
return true;
78+
}
79+
}
80+
81+
/// <summary>
82+
/// A factory class for instances of IOrderedEnumerableSerializer&lt;TItem>.
83+
/// </summary>
84+
public static class IOrderedEnumerableSerializer
85+
{
86+
/// <summary>
87+
/// Creates an instance IOrderedEnumerableSerializer&lt;TItem>.
88+
/// </summary>
89+
/// <param name="itemSerializer">The item serializer.</param>
90+
/// <param name="thenByExceptionMessage">The message to use when throwing an exception because ThenBy is not supported.</param>
91+
/// <returns>An IOrderedEnumerableSerializer&lt;TItem>.</returns>
92+
public static IBsonSerializer Create(IBsonSerializer itemSerializer, string thenByExceptionMessage)
93+
{
94+
if (itemSerializer == null) { throw new ArgumentNullException(nameof(itemSerializer)); }
95+
var itemType = itemSerializer.ValueType;
96+
var serializerType = typeof(IOrderedEnumerableSerializer<>).MakeGenericType(itemType);
97+
return (IBsonSerializer)Activator.CreateInstance(serializerType, itemSerializer, thenByExceptionMessage);
98+
}
99+
}
100+
101+
internal class OrderedEnumerableListWrapper<T> : IOrderedEnumerable<T>
102+
{
103+
private readonly List<T> _list;
104+
private readonly string _thenByExceptionMessage;
105+
106+
public OrderedEnumerableListWrapper(List<T> list, string thenByExceptionMessage)
107+
{
108+
_list = list;
109+
_thenByExceptionMessage = thenByExceptionMessage;
110+
}
111+
112+
public IOrderedEnumerable<T> CreateOrderedEnumerable<TKey>(Func<T, TKey> keySelector, IComparer<TKey> comparer, bool descending)
113+
{
114+
throw new InvalidOperationException(_thenByExceptionMessage);
115+
}
116+
117+
public IEnumerator<T> GetEnumerator()
118+
{
119+
return _list.GetEnumerator();
120+
}
121+
122+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
123+
}
124+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public class Feature
110110
private static readonly Feature __setWindowFieldsLocf = new Feature("SetWindowFieldsLocf", WireVersion.Server52);
111111
private static readonly Feature __shardedTransactions = new Feature("ShardedTransactions", WireVersion.Server42);
112112
private static readonly Feature __snapshotReads = new Feature("SnapshotReads", WireVersion.Server50, notSupportedMessage: "Snapshot reads require MongoDB 5.0 or later");
113+
private static readonly Feature __sortArrayOperator = new Feature("SortArrayOperator", WireVersion.Server52);
113114
private static readonly Feature __speculativeAuthentication = new Feature("SpeculativeAuthentication", WireVersion.Server44);
114115
private static readonly Feature __streamingHello = new Feature("StreamingHello", WireVersion.Server44);
115116
private static readonly Feature __tailableCursor = new Feature("TailableCursor", WireVersion.Server32);
@@ -606,6 +607,11 @@ public class Feature
606607
/// </summary>
607608
public static Feature SnapshotReads => __snapshotReads;
608609

610+
/// <summary>
611+
/// Gets the $sortArray operator feature.
612+
/// </summary>
613+
public static Feature SortArrayOperator => __sortArrayOperator;
614+
609615
/// <summary>
610616
/// Gets the speculative authentication feature.
611617
/// </summary>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ internal enum AstNodeType
130130
SizeFilterOperation,
131131
SkipStage,
132132
SliceExpression,
133+
SortArrayExpression,
133134
SortStage,
134135
SortByCountStage,
135136
SwitchExpression,

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,6 +692,21 @@ public static AstExpression Slice(AstExpression array, AstExpression position, A
692692
return new AstSliceExpression(array, position, n);
693693
}
694694

695+
public static AstExpression SortArray(AstExpression input, AstSortFields fields)
696+
{
697+
return new AstSortArrayExpression(input, fields);
698+
}
699+
700+
public static AstExpression SortArray(AstExpression input, params AstSortField[] fields)
701+
{
702+
return new AstSortArrayExpression(input, new AstSortFields(fields));
703+
}
704+
705+
public static AstExpression SortArray(AstExpression input, AstSortOrder order)
706+
{
707+
return new AstSortArrayExpression(input, order);
708+
}
709+
695710
public static AstExpression Split(AstExpression arg, AstExpression delimiter)
696711
{
697712
return new AstBinaryExpression(AstBinaryOperator.Split, arg, delimiter);
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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;
17+
using MongoDB.Bson;
18+
using MongoDB.Driver.Core.Misc;
19+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Visitors;
20+
21+
namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions
22+
{
23+
internal sealed class AstSortArrayExpression : AstExpression
24+
{
25+
private readonly AstSortFields _fields;
26+
private readonly AstExpression _input;
27+
private readonly AstSortOrder _order;
28+
29+
public AstSortArrayExpression(AstExpression input, AstSortFields fields)
30+
{
31+
_input = Ensure.IsNotNull(input, nameof(input));
32+
_fields = Ensure.IsNotNull(fields, nameof(fields));
33+
}
34+
35+
public AstSortArrayExpression(AstExpression input, AstSortOrder order)
36+
{
37+
_input = Ensure.IsNotNull(input, nameof(input));
38+
_order = Ensure.IsNotNull(order, nameof(order));
39+
}
40+
41+
public AstSortFields Fields => _fields;
42+
public AstExpression Input => _input;
43+
public override AstNodeType NodeType => AstNodeType.SortArrayExpression;
44+
public AstSortOrder Order => _order;
45+
46+
public override AstNode Accept(AstNodeVisitor visitor)
47+
{
48+
return visitor.VisitSortArrayExpression(this);
49+
}
50+
51+
public override BsonValue Render()
52+
{
53+
return new BsonDocument(
54+
"$sortArray",
55+
new BsonDocument
56+
{
57+
{ "input", _input.Render() },
58+
{ "sortBy", _fields?.Render(), _fields != null },
59+
{ "sortBy", _order?.Render(), _order != null }
60+
});
61+
}
62+
63+
public AstSortArrayExpression Update(AstExpression input, AstSortFields fields, AstSortOrder order)
64+
{
65+
if (input == _input && fields == _fields && order == _order)
66+
{
67+
return this;
68+
}
69+
70+
if (fields != null && order != null)
71+
{
72+
throw new ArgumentException("fields and order arguments are mutually exclusive.");
73+
}
74+
75+
return order == null ? new AstSortArrayExpression(input, fields) : new AstSortArrayExpression(input, order);
76+
}
77+
}
78+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,11 @@ public virtual AstNode VisitSliceExpression(AstSliceExpression node)
684684
return node.Update(VisitAndConvert(node.Array), VisitAndConvert(node.Position), VisitAndConvert(node.N));
685685
}
686686

687+
public virtual AstNode VisitSortArrayExpression(AstSortArrayExpression node)
688+
{
689+
return node.Update(VisitAndConvert(node.Input), node.Fields, node.Order);
690+
}
691+
687692
public virtual AstNode VisitSortByCountStage(AstSortByCountStage node)
688693
{
689694
return node.Update(VisitAndConvert(node.Expression));

src/MongoDB.Driver/Linq/Linq3Implementation/Misc/LambdaExpressionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public static string GetFieldPath(this LambdaExpression fieldSelectorLambda, Tra
4545
}
4646
}
4747

48-
throw new ExpressionNotSupportedException(fieldSelectorLambda);
48+
throw new ExpressionNotSupportedException(fieldSelectorLambda, because: "expression cannot be translated as a field path");
4949
}
5050
}
5151
}

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ public static AggregationExpression Translate(TranslationContext context, Method
122122
case "Min":
123123
return MaxOrMinMethodToAggregationExpressionTranslator.Translate(context, expression);
124124

125+
case "OrderBy":
126+
case "OrderByDescending":
127+
case "ThenBy":
128+
case "ThenByDescending":
129+
return OrderByMethodToAggregationExpressionTranslator.Translate(context, expression);
130+
125131
case "StandardDeviationPopulation":
126132
case "StandardDeviationSample":
127133
return StandardDeviationMethodsToAggregationExpressionTranslator.Translate(context, expression);

0 commit comments

Comments
 (0)