Skip to content

Commit bc008db

Browse files
committed
CSHARP-4443: Add comprehensive dictionary LINQ support for all 3 representations
1 parent d1cf901 commit bc008db

File tree

12 files changed

+508
-99
lines changed

12 files changed

+508
-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: 159 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;
@@ -71,11 +70,21 @@ public static TranslatedExpression Translate(TranslationContext context, MemberE
7170

7271
if (!DocumentSerializerHelper.AreMembersRepresentedAsFields(containerTranslation.Serializer, out _))
7372
{
74-
if (member is PropertyInfo propertyInfo && propertyInfo.Name == "Length")
73+
if (member is PropertyInfo propertyInfo && propertyInfo.Name == "Length")
7574
{
7675
return LengthPropertyToAggregationExpressionTranslator.Translate(context, expression);
7776
}
7877

78+
if (TryTranslateDictionaryProperty(expression, containerTranslation, member, out var translatedDictionaryProperty))
79+
{
80+
return translatedDictionaryProperty;
81+
}
82+
83+
if (TryTranslateKeyValuePairProperty(expression, containerTranslation, member, out var translatedKeyValuePairProperty))
84+
{
85+
return translatedKeyValuePairProperty;
86+
}
87+
7988
if (TryTranslateCollectionCountProperty(expression, containerTranslation, member, out var translatedCount))
8089
{
8190
return translatedCount;
@@ -126,11 +135,20 @@ private static bool TryTranslateCollectionCountProperty(MemberExpression express
126135
{
127136
if (EnumerableProperty.IsCountProperty(expression))
128137
{
129-
SerializationHelper.EnsureRepresentationIsArray(expression, container.Serializer);
138+
AstExpression ast;
130139

131-
var ast = AstExpression.Size(container.Ast);
132-
var serializer = Int32Serializer.Instance;
140+
if (container.Serializer is IBsonDictionarySerializer dictionarySerializer &&
141+
dictionarySerializer.DictionaryRepresentation == DictionaryRepresentation.Document)
142+
{
143+
ast = AstExpression.Size(AstExpression.ObjectToArray(container.Ast));
144+
}
145+
else
146+
{
147+
SerializationHelper.EnsureRepresentationIsArray(expression, container.Serializer);
148+
ast = AstExpression.Size(container.Ast);
149+
}
133150

151+
var serializer = Int32Serializer.Instance;
134152
result = new TranslatedExpression(expression, ast, serializer);
135153
return true;
136154
}
@@ -261,5 +279,141 @@ private static bool TryTranslateDictionaryProperty(TranslationContext context, M
261279
translatedDictionaryProperty = null;
262280
return false;
263281
}
282+
283+
private static bool TryTranslateKeyValuePairProperty(MemberExpression expression, TranslatedExpression container, MemberInfo memberInfo, out TranslatedExpression result)
284+
{
285+
result = null;
286+
287+
if (container.Expression.Type.IsGenericType &&
288+
container.Expression.Type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>) &&
289+
container.Serializer is IKeyValuePairSerializerV2 { Representation: BsonType.Array } kvpSerializer)
290+
{
291+
AstExpression ast;
292+
IBsonSerializer serializer;
293+
294+
switch (memberInfo.Name)
295+
{
296+
case "Key":
297+
ast = AstExpression.ArrayElemAt(container.Ast, 0);
298+
serializer = kvpSerializer.KeySerializer;
299+
break;
300+
case "Value":
301+
ast = AstExpression.ArrayElemAt(container.Ast, 1);
302+
serializer = kvpSerializer.ValueSerializer;
303+
break;
304+
default:
305+
throw new ExpressionNotSupportedException(expression);
306+
}
307+
result = new TranslatedExpression(expression, ast, serializer);
308+
return true;
309+
}
310+
311+
return false;
312+
}
313+
314+
private static bool TryTranslateDictionaryProperty(MemberExpression expression, TranslatedExpression container, MemberInfo memberInfo, out TranslatedExpression result)
315+
{
316+
result = null;
317+
318+
if (memberInfo is PropertyInfo propertyInfo &&
319+
propertyInfo.DeclaringType.IsGenericType &&
320+
(propertyInfo.DeclaringType.GetGenericTypeDefinition() == typeof(Dictionary<,>) ||
321+
propertyInfo.DeclaringType.GetGenericTypeDefinition() == typeof(IDictionary<,>)))
322+
{
323+
if (container.Serializer is IBsonDictionarySerializer dictionarySerializer)
324+
{
325+
switch (propertyInfo.Name)
326+
{
327+
case "Count":
328+
{
329+
AstExpression countAst;
330+
switch (dictionarySerializer.DictionaryRepresentation)
331+
{
332+
case DictionaryRepresentation.Document:
333+
countAst = AstExpression.Size(AstExpression.ObjectToArray(container.Ast));
334+
break;
335+
case DictionaryRepresentation.ArrayOfArrays:
336+
case DictionaryRepresentation.ArrayOfDocuments:
337+
countAst = AstExpression.Size(container.Ast);
338+
break;
339+
default:
340+
throw new ExpressionNotSupportedException(expression);
341+
}
342+
343+
var serializer = Int32Serializer.Instance;
344+
result = new TranslatedExpression(expression, countAst, serializer);
345+
return true;
346+
}
347+
348+
case "Keys":
349+
{
350+
AstExpression keysAst;
351+
switch (dictionarySerializer.DictionaryRepresentation)
352+
{
353+
case DictionaryRepresentation.Document:
354+
keysAst = AstExpression.GetField(AstExpression.ObjectToArray(container.Ast), "k");
355+
break;
356+
case DictionaryRepresentation.ArrayOfArrays:
357+
{
358+
var kvp = AstExpression.Var("kvp");
359+
keysAst = AstExpression.Map(
360+
input: container.Ast,
361+
@as: kvp,
362+
@in: AstExpression.ArrayElemAt(kvp, 0));
363+
break;
364+
}
365+
case DictionaryRepresentation.ArrayOfDocuments:
366+
keysAst = AstExpression.GetField(container.Ast, "k");
367+
break;
368+
369+
default:
370+
throw new ExpressionNotSupportedException(expression);
371+
}
372+
373+
var serializer = ArraySerializerHelper.CreateSerializer(dictionarySerializer.KeySerializer);
374+
result = new TranslatedExpression(expression, keysAst, serializer);
375+
return true;
376+
}
377+
378+
case "Values":
379+
{
380+
AstExpression valuesAst;
381+
switch (dictionarySerializer.DictionaryRepresentation)
382+
{
383+
case DictionaryRepresentation.Document:
384+
valuesAst = AstExpression.GetField(AstExpression.ObjectToArray(container.Ast), "v");
385+
break;
386+
case DictionaryRepresentation.ArrayOfArrays:
387+
{
388+
var kvp = AstExpression.Var("kvp");
389+
valuesAst = AstExpression.Map(
390+
input: container.Ast,
391+
@as: kvp,
392+
@in: AstExpression.ArrayElemAt(kvp, 1));
393+
break;
394+
}
395+
case DictionaryRepresentation.ArrayOfDocuments:
396+
valuesAst = AstExpression.GetField(container.Ast, "v");
397+
break;
398+
399+
default:
400+
throw new ExpressionNotSupportedException(expression);
401+
}
402+
403+
var serializer = ArraySerializerHelper.CreateSerializer(dictionarySerializer.ValueSerializer);
404+
result = new TranslatedExpression(expression, valuesAst, serializer);
405+
return true;
406+
}
407+
408+
default:
409+
throw new ExpressionNotSupportedException(expression);
410+
}
411+
}
412+
413+
throw new ExpressionNotSupportedException(expression, because: "serializer does not implement IBsonDictionarySerializer");
414+
}
415+
416+
return false;
417+
}
264418
}
265419
}

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)