Skip to content

Commit b66daf1

Browse files
committed
CSHARP-4443: Add comprehensive dictionary LINQ support for all 3 representations
1 parent 17c8263 commit b66daf1

File tree

12 files changed

+509
-99
lines changed

12 files changed

+509
-99
lines changed

src/MongoDB.Bson/Serialization/Serializers/KeyValuePairSerializer.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ public interface IKeyValuePairSerializer
2929
BsonType Representation { get; }
3030
}
3131

32+
/// <summary>
33+
/// An extended interface for KeyValuePairSerializer that provides access to key and value serializers.
34+
/// </summary>
35+
public interface IKeyValuePairSerializerV2 : IKeyValuePairSerializer
36+
{
37+
/// <summary>
38+
/// Gets the key serializer.
39+
/// </summary>
40+
IBsonSerializer KeySerializer { get; }
41+
42+
/// <summary>
43+
/// Gets the value serializer.
44+
/// </summary>
45+
IBsonSerializer ValueSerializer { get; }
46+
}
47+
3248
/// <summary>
3349
/// Static factory class for KeyValuePairSerializers.
3450
/// </summary>
@@ -61,7 +77,7 @@ public static IBsonSerializer Create(
6177
public sealed class KeyValuePairSerializer<TKey, TValue> :
6278
StructSerializerBase<KeyValuePair<TKey, TValue>>,
6379
IBsonDocumentSerializer,
64-
IKeyValuePairSerializer
80+
IKeyValuePairSerializerV2
6581
{
6682
// private constants
6783
private static class Flags
@@ -191,6 +207,16 @@ public IBsonSerializer<TValue> ValueSerializer
191207
get { return _lazyValueSerializer.Value; }
192208
}
193209

210+
/// <summary>
211+
/// Gets the key serializer.
212+
/// </summary>
213+
IBsonSerializer IKeyValuePairSerializerV2.KeySerializer => KeySerializer;
214+
215+
/// <summary>
216+
/// Gets the value serializer.
217+
/// </summary>
218+
IBsonSerializer IKeyValuePairSerializerV2.ValueSerializer => ValueSerializer;
219+
194220
// public methods
195221
/// <summary>
196222
/// Deserializes a value.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ public static AstExpression ArrayElemAt(AstExpression array, AstExpression index
134134
return new AstBinaryExpression(AstBinaryOperator.ArrayElemAt, array, index);
135135
}
136136

137+
public static AstExpression ArrayToObject(AstExpression arg)
138+
{
139+
return new AstUnaryExpression(AstUnaryOperator.ArrayToObject, arg);
140+
}
141+
137142
public static AstExpression Avg(AstExpression array)
138143
{
139144
return new AstUnaryExpression(AstUnaryOperator.Avg, array);

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

Lines changed: 160 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
*/
1515

1616
using System;
17-
using System.Collections;
1817
using System.Collections.Generic;
1918
using System.Linq.Expressions;
2019
using System.Reflection;
@@ -66,11 +65,21 @@ public static TranslatedExpression Translate(TranslationContext context, MemberE
6665

6766
if (!DocumentSerializerHelper.AreMembersRepresentedAsFields(containerTranslation.Serializer, out _))
6867
{
69-
if (member is PropertyInfo propertyInfo && propertyInfo.Name == "Length")
68+
if (member is PropertyInfo propertyInfo && propertyInfo.Name == "Length")
7069
{
7170
return LengthPropertyToAggregationExpressionTranslator.Translate(context, expression);
7271
}
7372

73+
if (TryTranslateDictionaryProperty(expression, containerTranslation, member, out var translatedDictionaryProperty))
74+
{
75+
return translatedDictionaryProperty;
76+
}
77+
78+
if (TryTranslateKeyValuePairProperty(expression, containerTranslation, member, out var translatedKeyValuePairProperty))
79+
{
80+
return translatedKeyValuePairProperty;
81+
}
82+
7483
if (TryTranslateCollectionCountProperty(expression, containerTranslation, member, out var translatedCount))
7584
{
7685
return translatedCount;
@@ -121,11 +130,20 @@ private static bool TryTranslateCollectionCountProperty(MemberExpression express
121130
{
122131
if (EnumerableProperty.IsCountProperty(expression))
123132
{
124-
SerializationHelper.EnsureRepresentationIsArray(expression, container.Serializer);
133+
AstExpression ast;
125134

126-
var ast = AstExpression.Size(container.Ast);
127-
var serializer = Int32Serializer.Instance;
135+
if (container.Serializer is IBsonDictionarySerializer dictionarySerializer &&
136+
dictionarySerializer.DictionaryRepresentation == DictionaryRepresentation.Document)
137+
{
138+
ast = AstExpression.Size(AstExpression.ObjectToArray(container.Ast));
139+
}
140+
else
141+
{
142+
SerializationHelper.EnsureRepresentationIsArray(expression, container.Serializer);
143+
ast = AstExpression.Size(container.Ast);
144+
}
128145

146+
var serializer = Int32Serializer.Instance;
129147
result = new TranslatedExpression(expression, ast, serializer);
130148
return true;
131149
}
@@ -187,5 +205,142 @@ private static bool TryTranslateDateTimeProperty(MemberExpression expression, Tr
187205

188206
return false;
189207
}
208+
209+
private static bool TryTranslateKeyValuePairProperty(MemberExpression expression, TranslatedExpression container, MemberInfo memberInfo, out TranslatedExpression result)
210+
{
211+
result = null;
212+
213+
if (container.Expression.Type.IsGenericType &&
214+
container.Expression.Type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>) &&
215+
container.Serializer is IKeyValuePairSerializerV2 { Representation: BsonType.Array } kvpSerializer)
216+
{
217+
AstExpression ast;
218+
IBsonSerializer serializer;
219+
220+
switch (memberInfo.Name)
221+
{
222+
case "Key":
223+
ast = AstExpression.ArrayElemAt(container.Ast, 0);
224+
serializer = kvpSerializer.KeySerializer;
225+
break;
226+
case "Value":
227+
ast = AstExpression.ArrayElemAt(container.Ast, 1);
228+
serializer = kvpSerializer.ValueSerializer;
229+
break;
230+
default:
231+
throw new ExpressionNotSupportedException(expression);
232+
}
233+
result = new TranslatedExpression(expression, ast, serializer);
234+
return true;
235+
}
236+
237+
return false;
238+
}
239+
240+
private static bool TryTranslateDictionaryProperty(MemberExpression expression, TranslatedExpression container, MemberInfo memberInfo, out TranslatedExpression result)
241+
{
242+
result = null;
243+
244+
if (memberInfo is PropertyInfo propertyInfo &&
245+
propertyInfo.DeclaringType.IsGenericType &&
246+
(propertyInfo.DeclaringType.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
247+
propertyInfo.DeclaringType.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
248+
{
249+
if (container.Serializer is IBsonDictionarySerializer dictionarySerializer)
250+
{
251+
switch (propertyInfo.Name)
252+
{
253+
case "Count":
254+
{
255+
AstExpression countAst;
256+
switch (dictionarySerializer.DictionaryRepresentation)
257+
{
258+
case DictionaryRepresentation.Document:
259+
countAst = AstExpression.Size(AstExpression.ObjectToArray(container.Ast));
260+
break;
261+
case DictionaryRepresentation.ArrayOfArrays:
262+
case DictionaryRepresentation.ArrayOfDocuments:
263+
countAst = AstExpression.Size(container.Ast);
264+
break;
265+
default:
266+
throw new ExpressionNotSupportedException(expression);
267+
}
268+
269+
var serializer = Int32Serializer.Instance;
270+
result = new TranslatedExpression(expression, countAst, serializer);
271+
return true;
272+
}
273+
274+
case "Keys":
275+
{
276+
AstExpression keysAst;
277+
switch (dictionarySerializer.DictionaryRepresentation)
278+
{
279+
case DictionaryRepresentation.Document:
280+
keysAst = AstExpression.GetField(AstExpression.ObjectToArray(container.Ast), "k");
281+
break;
282+
case DictionaryRepresentation.ArrayOfArrays:
283+
{
284+
var kvp = AstExpression.Var("kvp");
285+
keysAst = AstExpression.Map(
286+
input: container.Ast,
287+
@as: kvp,
288+
@in: AstExpression.ArrayElemAt(kvp, 0));
289+
break;
290+
}
291+
case DictionaryRepresentation.ArrayOfDocuments:
292+
keysAst = AstExpression.GetField(container.Ast, "k");
293+
break;
294+
295+
default:
296+
throw new ExpressionNotSupportedException(expression);
297+
}
298+
299+
var serializer = ArraySerializerHelper.CreateSerializer(dictionarySerializer.KeySerializer);
300+
result = new TranslatedExpression(expression, keysAst, serializer);
301+
return true;
302+
}
303+
304+
case "Values":
305+
{
306+
AstExpression valuesAst;
307+
switch (dictionarySerializer.DictionaryRepresentation)
308+
{
309+
case DictionaryRepresentation.Document:
310+
valuesAst = AstExpression.GetField(AstExpression.ObjectToArray(container.Ast), "v");
311+
break;
312+
case DictionaryRepresentation.ArrayOfArrays:
313+
{
314+
var kvp = AstExpression.Var("kvp");
315+
valuesAst = AstExpression.Map(
316+
input: container.Ast,
317+
@as: kvp,
318+
@in: AstExpression.ArrayElemAt(kvp, 1));
319+
break;
320+
}
321+
case DictionaryRepresentation.ArrayOfDocuments:
322+
valuesAst = AstExpression.GetField(container.Ast, "v");
323+
break;
324+
325+
default:
326+
throw new ExpressionNotSupportedException(expression);
327+
}
328+
329+
var serializer = ArraySerializerHelper.CreateSerializer(dictionarySerializer.ValueSerializer);
330+
result = new TranslatedExpression(expression, valuesAst, serializer);
331+
return true;
332+
}
333+
334+
default:
335+
throw new ExpressionNotSupportedException(expression);
336+
}
337+
}
338+
339+
throw new ExpressionNotSupportedException(expression, because: "serializer does not implement IBsonDictionarySerializer");
340+
}
341+
342+
return false;
343+
}
344+
190345
}
191346
}

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ContainsKeyMethodToAggregationExpressionTranslator.cs

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,65 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC
3636
{
3737
var dictionaryExpression = expression.Object;
3838
var keyExpression = arguments[0];
39+
return TranslateContainsKey(context, expression, dictionaryExpression, keyExpression);
40+
}
41+
42+
throw new ExpressionNotSupportedException(expression);
43+
}
3944

40-
var dictionaryTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, dictionaryExpression);
41-
var dictionarySerializer = GetDictionarySerializer(expression, dictionaryTranslation);
42-
var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation;
45+
public static TranslatedExpression TranslateContainsKey(TranslationContext context, Expression expression, Expression dictionaryExpression, Expression keyExpression)
46+
{
47+
var dictionaryTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, dictionaryExpression);
48+
var dictionarySerializer = GetDictionarySerializer(expression, dictionaryTranslation);
49+
var dictionaryRepresentation = dictionarySerializer.DictionaryRepresentation;
4350

44-
AstExpression ast;
45-
switch (dictionaryRepresentation)
46-
{
47-
case DictionaryRepresentation.Document:
51+
AstExpression ast;
52+
switch (dictionaryRepresentation)
53+
{
54+
case DictionaryRepresentation.Document:
55+
{
4856
var keyFieldName = GetKeyFieldName(context, expression, keyExpression, dictionarySerializer.KeySerializer);
4957
ast = AstExpression.IsNotMissing(AstExpression.GetField(dictionaryTranslation.Ast, keyFieldName));
5058
break;
59+
}
5160

52-
default:
53-
throw new ExpressionNotSupportedException(expression, because: $"ContainsKey is not supported when DictionaryRepresentation is: {dictionaryRepresentation}");
54-
}
61+
case DictionaryRepresentation.ArrayOfDocuments:
62+
{
63+
var keyFieldName = GetKeyFieldName(context, expression, keyExpression, dictionarySerializer.KeySerializer);
64+
var (valueBinding, valueAst) = AstExpression.UseVarIfNotSimple("value", keyFieldName);
65+
ast = AstExpression.Let(
66+
var: valueBinding,
67+
@in: AstExpression.Reduce(
68+
input: dictionaryTranslation.Ast,
69+
initialValue: false,
70+
@in: AstExpression.Cond(
71+
@if: AstExpression.Var("value"),
72+
@then: true,
73+
@else: AstExpression.Eq(AstExpression.GetField(AstExpression.Var("this"), "k"), valueAst))));
74+
break;
75+
}
76+
77+
case DictionaryRepresentation.ArrayOfArrays:
78+
{
79+
var keyFieldName = GetKeyFieldName(context, expression, keyExpression, dictionarySerializer.KeySerializer);
80+
var (valueBinding, valueAst) = AstExpression.UseVarIfNotSimple("value", keyFieldName);
81+
ast = AstExpression.Let(
82+
var: valueBinding,
83+
@in: AstExpression.Reduce(
84+
input: dictionaryTranslation.Ast,
85+
initialValue: false,
86+
@in: AstExpression.Cond(
87+
@if: AstExpression.Var("value"),
88+
@then: true,
89+
@else: AstExpression.Eq(AstExpression.ArrayElemAt(AstExpression.Var("this"), 0), valueAst))));
90+
break;
91+
}
5592

56-
return new TranslatedExpression(expression, ast, BooleanSerializer.Instance);
93+
default:
94+
throw new ExpressionNotSupportedException(expression, because: $"DictionaryRepresentation: {dictionaryRepresentation} is not supported.");
5795
}
5896

59-
throw new ExpressionNotSupportedException(expression);
97+
return new TranslatedExpression(expression, ast, BooleanSerializer.Instance);
6098
}
6199

62100
private static AstExpression GetKeyFieldName(TranslationContext context, Expression expression, Expression keyExpression, IBsonSerializer keySerializer)

0 commit comments

Comments
 (0)