Skip to content

CSHARP-4779: Support Dictionary(IEnumerable<KeyValuePair<TKey, TValue>> collection) constructor in LINQ… #1657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

using System;
using System.Collections.Generic;
using System.Linq;
using MongoDB.Bson;
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
Expand Down Expand Up @@ -454,6 +455,79 @@ public override AstNode VisitMapExpression(AstMapExpression node)
}
}

// { $map : { input : { $map : { input : <innerInput>, as : "inner", in : { A : <exprA>, B : <exprB>, ... } } }, as: "outer", in : { F : '$$outer.A', G : "$$outer.B", ... } } }
// => { $map : { input : <innerInput>, as: "inner", in : { F : <exprA>, G : <exprB>, ... } } }
if (node.Input is AstMapExpression innerMapExpression &&
node.As is var outerVar &&
node.In is AstComputedDocumentExpression outerComputedDocumentExpression &&
innerMapExpression.Input is var innerInput &&
innerMapExpression.As is var innerVar &&
innerMapExpression.In is AstComputedDocumentExpression innerComputedDocumentExpression &&
outerComputedDocumentExpression.Fields.All(outerField =>
outerField.Value is AstGetFieldExpression outerGetFieldExpression &&
outerGetFieldExpression.Input == outerVar &&
outerGetFieldExpression.FieldName is AstConstantExpression { Value : BsonString { Value : var matchingFieldName } } &&
innerComputedDocumentExpression.Fields.Any(innerField => innerField.Path == matchingFieldName)))
{
var rewrittenOuterFields = new List<AstComputedField>();
foreach (var outerField in outerComputedDocumentExpression.Fields)
{
var outerGetFieldExpression = (AstGetFieldExpression)outerField.Value;
var matchingFieldName = ((AstConstantExpression)outerGetFieldExpression.FieldName).Value.AsString;
var matchingInnerField = innerComputedDocumentExpression.Fields.Single(innerField => innerField.Path == matchingFieldName);
var rewrittenOuterField = AstExpression.ComputedField(outerField.Path, matchingInnerField.Value);
rewrittenOuterFields.Add(rewrittenOuterField);
}

var simplified = AstExpression.Map(
input: innerInput,
@as: innerVar,
@in: AstExpression.ComputedDocument(rewrittenOuterFields));

return Visit(simplified);
}

// { $map : { input : [{ A : <exprA1>, B : <exprB1>, ... }, { A : <exprA2>, B : <exprB2>, ... }, ...], as : "item", in: { F : "$$item.A", G : "$$item.B", ... } } }
// => [{ F : <exprA1>, G : <exprB1>", ... }, { F : <exprA2>, G : <exprB2>, ... }, ...]
if (node.Input is AstComputedArrayExpression inputComputedArray &&
inputComputedArray.Items.Count >= 1 &&
inputComputedArray.Items[0] is AstComputedDocumentExpression firstComputedDocument &&
firstComputedDocument.Fields.Select(inputField => inputField.Path).ToArray() is var inputFieldNames &&
inputComputedArray.Items.Skip(1).All(otherItem =>
otherItem is AstComputedDocumentExpression otherComputedDocument &&
otherComputedDocument.Fields.Select(otherField => otherField.Path).SequenceEqual(inputFieldNames)) &&
node.As is var itemVar &&
node.In is AstComputedDocumentExpression mappedDocument &&
mappedDocument.Fields.All(mappedField =>
mappedField.Value is AstGetFieldExpression mappedGetField &&
mappedGetField.Input == itemVar &&
mappedGetField.FieldName is AstConstantExpression { Value : BsonString { Value : var matchingFieldName } } &&
inputFieldNames.Contains(matchingFieldName)))
{
var rewrittenItems = new List<AstExpression>();
foreach (var inputItem in inputComputedArray.Items)
{
var inputDocument = (AstComputedDocumentExpression)inputItem;

var rewrittenFields = new List<AstComputedField>();
foreach (var mappedField in mappedDocument.Fields)
{
var mappedGetField = (AstGetFieldExpression)mappedField.Value;
var matchingFieldName = ((AstConstantExpression)mappedGetField.FieldName).Value.AsString;
var matchingInputField = inputDocument.Fields.Single(inputField => inputField.Path == matchingFieldName);
var rewrittenField = AstExpression.ComputedField(mappedField.Path, matchingInputField.Value);
rewrittenFields.Add(rewrittenField);
}

var rewrittenItem = AstExpression.ComputedDocument(rewrittenFields);
rewrittenItems.Add(rewrittenItem);
}

var simplified = AstExpression.ComputedArray(rewrittenItems);

return Visit(simplified);
}

return base.VisitMapExpression(node);

static AstExpression UltimateGetFieldInput(AstGetFieldExpression getField)
Expand Down Expand Up @@ -574,7 +648,32 @@ arg is AstBinaryExpression argBinaryExpression &&
return AstExpression.Binary(oppositeComparisonOperator, argBinaryExpression.Arg1, argBinaryExpression.Arg2);
}

// { $arrayToObject : [[{ k : 'A', v : <exprA> }, { k : 'B', v : <exprB> }, ...]] } => { A : <exprA>, B : <exprB>, ... }
if (node.Operator == AstUnaryOperator.ArrayToObject &&
arg is AstComputedArrayExpression computedArrayExpression &&
computedArrayExpression.Items.All(
item =>
item is AstComputedDocumentExpression computedDocumentExpression &&
computedDocumentExpression.Fields.Count == 2 &&
computedDocumentExpression.Fields[0].Path == "k" &&
computedDocumentExpression.Fields[1].Path == "v" &&
computedDocumentExpression.Fields[0].Value is AstConstantExpression { Value : { IsString : true } }))
{
var computedFields = computedArrayExpression.Items.Select(KeyValuePairDocumentToComputedField);
return AstExpression.ComputedDocument(computedFields);
}

return node.Update(arg);

static AstComputedField KeyValuePairDocumentToComputedField(AstExpression expression)
{
// caller has verified that expression is of the form: { k : <stringConstant>, v : <valueExpression> }
var keyValuePairDocumentExpression = (AstComputedDocumentExpression)expression;
var keyConstantExpression = (AstConstantExpression)keyValuePairDocumentExpression.Fields[0].Value;
var valueExpression = keyValuePairDocumentExpression.Fields[1].Value;

return AstExpression.ComputedField(keyConstantExpression.Value.AsString, valueExpression);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System.Collections.Generic;
using System.Reflection;
using MongoDB.Driver.Linq.Linq3Implementation.Misc;

namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection
{
internal static class DictionaryConstructor
{
public static bool IsWithIEnumerableKeyValuePairConstructor(ConstructorInfo constructor)
{
var declaringType = constructor.DeclaringType;
var parameters = constructor.GetParameters();
return
declaringType.IsConstructedGenericType &&
declaringType.GetGenericTypeDefinition() == typeof(Dictionary<,>) &&
parameters.Length == 1 &&
parameters[0].ParameterType.ImplementsIEnumerable(out var enumerableType) &&
enumerableType.IsConstructedGenericType &&
enumerableType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Options;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;

namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators
{
internal static class NewDictionaryExpressionToAggregationExpressionTranslator
{
public static bool CanTranslate(NewExpression expression)
=> DictionaryConstructor.IsWithIEnumerableKeyValuePairConstructor(expression.Constructor);

public static TranslatedExpression Translate(TranslationContext context, NewExpression expression)
{
var arguments = expression.Arguments;

var collectionExpression = arguments[0];
var collectionTranslation = ExpressionToAggregationExpressionTranslator.TranslateEnumerable(context, collectionExpression);
var itemSerializer = ArraySerializerHelper.GetItemSerializer(collectionTranslation.Serializer);

IBsonSerializer keySerializer;
IBsonSerializer valueSerializer;
AstExpression collectionTranslationAst;
Copy link
Contributor

@adelinowona adelinowona Apr 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move this declaration inside the first if statement below

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


if (itemSerializer is IBsonDocumentSerializer itemDocumentSerializer)
{
if (!itemDocumentSerializer.TryGetMemberSerializationInfo("Key", out var keyMemberSerializationInfo))
{
throw new ExpressionNotSupportedException(expression, because: $"serializer class {itemSerializer.GetType()} does not have a Key member");
}
keySerializer = keyMemberSerializationInfo.Serializer;

if (!itemDocumentSerializer.TryGetMemberSerializationInfo("Value", out var valueMemberSerializationInfo))
{
throw new ExpressionNotSupportedException(expression, because: $"serializer class {itemSerializer.GetType()} does not have a Value member");
}
valueSerializer = valueMemberSerializationInfo.Serializer;

if (keyMemberSerializationInfo.ElementName == "k" && valueMemberSerializationInfo.ElementName == "v")
{
collectionTranslationAst = collectionTranslation.Ast;
}
else
{
var pairVar = AstExpression.Var("pair");
var computedDocumentAst = AstExpression.ComputedDocument([
AstExpression.ComputedField("k", AstExpression.GetField(pairVar, keyMemberSerializationInfo.ElementName)),
AstExpression.ComputedField("v", AstExpression.GetField(pairVar, valueMemberSerializationInfo.ElementName))
]);

collectionTranslationAst = AstExpression.Map(collectionTranslation.Ast, pairVar, computedDocumentAst);
}
}
else
{
throw new ExpressionNotSupportedException(expression);
}

if (keySerializer is not IRepresentationConfigurable { Representation: BsonType.String })
{
throw new ExpressionNotSupportedException(expression, because: "key does not serialize as a string");
}

var ast = AstExpression.Unary(AstUnaryOperator.ArrayToObject, collectionTranslationAst);
var resultSerializer = CreateResultSerializer(keySerializer, valueSerializer);
return new TranslatedExpression(expression, ast, resultSerializer);
}

private static IBsonSerializer CreateResultSerializer(IBsonSerializer keySerializer, IBsonSerializer valueSerializer)
{
var dictionaryType = typeof(Dictionary<,>).MakeGenericType(keySerializer.ValueType, valueSerializer.ValueType);
var serializerType = typeof(DictionaryInterfaceImplementerSerializer<,,>).MakeGenericType(dictionaryType, keySerializer.ValueType, valueSerializer.ValueType);

return (IBsonSerializer)Activator.CreateInstance(serializerType, DictionaryRepresentation.Document, keySerializer, valueSerializer);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ public static TranslatedExpression Translate(TranslationContext context, NewExpr
{
return NewKeyValuePairExpressionToAggregationExpressionTranslator.Translate(context, expression);
}
if (NewDictionaryExpressionToAggregationExpressionTranslator.CanTranslate(expression))
{
return NewDictionaryExpressionToAggregationExpressionTranslator.Translate(context, expression);
}
return MemberInitExpressionToAggregationExpressionTranslator.Translate(context, expression, expression, Array.Empty<MemberBinding>());
}
}
Expand Down
Loading