Skip to content

Conversation

@rstam
Copy link
Contributor

@rstam rstam commented Nov 7, 2025

No description provided.

@rstam rstam requested a review from adelinowona November 7, 2025 23:51
@rstam rstam added the improvement Optimizations or refactoring (no new features or fixes). label Nov 7, 2025
@rstam rstam requested a review from a team as a code owner November 7, 2025 23:51
return new AstComputedDocumentExpression(fields);
}

public static AstExpression ComputedDocument(IEnumerable<(string Name, AstExpression Value)> fields)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A more convenient overload of ComputedDocument.

}
}

internal class DictionaryKeyCollectionSerializer<TKey, TValue> : EnumerableSerializerBase<Dictionary<TKey, TValue>.KeyCollection>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need a serializer for Dictionary<TKey, TValue>.KeyCollection to be able to work with this type in LINQ queries or to deserialize them client-side.

}
}

internal class DictionaryValueCollectionSerializer<TKey, TValue> : SerializerBase<Dictionary<TKey, TValue>.ValueCollection>, IBsonArraySerializer
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We need a serializer for Dictionary<TKey, TValue>.ValueCollection to be able to work with this type in LINQ queries or to deserialize them client-side.

}
}

internal class DictionarySerializer<TKey, TValue> : DictionarySerializerBase<Dictionary<TKey, TValue>, TKey, TValue>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just a more convenient way to create a serializer for Dictionary<TKey, TValue>.

}
}

internal class ICollectionSerializer<TItem> : EnumerableSerializerBase<ICollection<TItem>>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just a more convenient way to create a serializer for ICollection<TItem>.

}
}

internal class KeyValuePairWrappedValueSerializer<TKey, TValue> : SerializerBase<TValue>, IWrappedValueSerializer
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a special serializer that deserializes a KeyValuePair but then extracts only the Value portion of it as the result.

It also represents values that are still coupled with the corresponding key as needed to work with a Dictionary<TKey, TValue>.ValueCollection.

{
// public static methods
public static TranslatedExpression Translate(TranslationContext context, Expression expression)
{
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Whenever we want to use a value we go ahead and unwrap it if it is wrapped.


var ast = AstExpression.ObjectToArray(aggregateExpression.Ast);
var ienumerableSerializer = ArraySerializerHelper.CreateSerializer(keyValuePairSerializer);
var arrayOfDocumentsDictionarySerializer = DictionarySerializer.Create(DictionaryRepresentation.ArrayOfDocuments, keySerializer, valueSerializer);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We were creating the "wrong" serializer before.

case "Values":
if (declaringTypeDefinition == typeof(Dictionary<,>))
{
var kvpPairsAst = dictionaryRepresentation switch
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The tricky part about supporting Values is that we have to keep the keys that go with them, at least until some later time where the keys might no longer be needed.

}
else if (declaringTypeDefinition == typeof(IDictionary<,>))
{
var valuesAst = dictionaryRepresentation switch
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Supporting Values for IDictionary<TKey, TValue> is MUCH more straightforward because the type is ICollection<TValue>.

@rstam rstam requested a review from sanych-sun November 8, 2025 00:02
case "Keys":
var keysAst = dictionaryRepresentation switch
{
DictionaryRepresentation.ArrayOfDocuments => AstExpression.Map(containerAst, kvpVar, AstExpression.GetField(kvpVar, "k")),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

By the time we get here we will never encounter DictionaryRepresentation.Document, because the call to TranslateEnumerable on line 204 above will have already used $objectToArray to transform the representation from Document to ArrayOfDocuments.

@adelinowona adelinowona requested a review from Copilot November 10, 2025 16:36
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This pull request implements support for Dictionary Keys and Values properties in MongoDB's LINQ provider, enabling these properties to be used in queries for different dictionary representations (ArrayOfArrays, ArrayOfDocuments, and Document).

Key Changes:

  • Added translation logic for Dictionary and IDictionary Keys and Values properties
  • Introduced specialized serializers to handle dictionary key and value collections during deserialization
  • Refactored expression translation to separate unwrapping logic from the main translation flow

Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
CSharp5779Tests.cs Comprehensive test coverage for Dictionary/IDictionary Keys/Values operations across different serialization formats
MemberExpressionToAggregationExpressionTranslator.cs Core translation logic for Dictionary Keys/Values properties with representation-specific AST generation
ExpressionToAggregationExpressionTranslator.cs Refactored to extract unwrapping logic and fixed serializer usage for objectToArray operations
KeyValuePairWrappedValueSerializer.cs New serializer for extracting values from KeyValuePair documents
ICollectionSerializer.cs Generic serializer for ICollection deserialization
DictionaryValueCollectionSerializer.cs Specialized serializer for Dictionary.ValueCollection
DictionarySerializer.cs Factory for creating dictionary serializers with specific representations
DictionaryKeyCollectionSerializer.cs Specialized serializer for Dictionary.KeyCollection
AstExpression.cs Added convenience overload for creating computed documents from tuples

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

.Select(x => x.IDictionaryAsArrayOfArrays.Values.First(v => v == 2));

var stages = Translate(collection, queryable);
AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } , as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }");
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

[nitpick] Extraneous space before the comma after the closing brace. The space before , as is inconsistent with other test assertions in this file.

Suggested change
AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } } , as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }");
AssertStages(stages, "{ $project : { _v : { $arrayElemAt : [{ $filter : { input : { $map : { input : '$IDictionaryAsArrayOfArrays', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 1] } } }, as : 'v', cond : { $eq : ['$$v', 2] } } }, 0] }, _id : 0 } }");

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

{
var valuesAst = dictionaryRepresentation switch
{
DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstComputedArrayExpression.ArrayElemAt(kvpVar, 1)),
Copy link

Copilot AI Nov 10, 2025

Choose a reason for hiding this comment

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

Inconsistent use of AstComputedArrayExpression.ArrayElemAt instead of AstExpression.ArrayElemAt. Line 220 uses AstExpression.ArrayElemAt for a similar operation, suggesting this should also use AstExpression.ArrayElemAt for consistency.

Suggested change
DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstComputedArrayExpression.ArrayElemAt(kvpVar, 1)),
DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstExpression.ArrayElemAt(kvpVar, 1)),

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with Copilot. Don't know how this happened... probably some weird Rider auto-completion that I didn't catch.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

return (IBsonSerializer)Activator.CreateInstance(serializerType, [keySerializer, valueSerializer]);
}
}

Copy link
Member

@sanych-sun sanych-sun Nov 10, 2025

Choose a reason for hiding this comment

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

Why DictionaryValueCollectionSerializer does not inherited from EnumerableSerializerBase as DictionaryKeyCollectionSerializer does?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe it could, but I think it would probably be more complicated. See the implementation of the Deserialize method in this class.

In essence it is because for DictionaryKeyCollectionSerializer we only ever need the key values, so EnumerableSerializerBase<TKey> makes sense.

But for DictionaryValueCollectionSerializer we need the key/value pairs (not just the values), EnumerableSerializerBase<TValue> doesn't work.


public DictionaryValueCollectionSerializer(IBsonSerializer<TKey> keySerializer, IBsonSerializer<TValue> valueSerializer)
{
_dictionarySerializer = (IBsonSerializer<Dictionary<TKey, TValue>>)DictionarySerializer.Create(DictionaryRepresentation.ArrayOfDocuments, keySerializer, valueSerializer);
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't we have DictionaryRepresentation as a parameter?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, because the representation MUST be ArrayOfDocuments.


namespace MongoDB.Driver.Linq.Linq3Implementation.Serializers;

internal static class DictionaryValueCollectionSerializer
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if I really understand the difference, but it feels like Key- and Value-Collections serializers must be exactly the same, but accessing the different property of the elements. If so, can we share code in between those serializers in some way?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They have in common that the only way to create them is to create a dictionary first and then access the .Keys or the .Values property. This is a weirdness of how .NET defined them...

They are different in that for creating the keys collection all we need are the key values. We can use default for the values, because the values don't matter.

But for the values collection we need to have unique key values. The key values don't matter, but they have to be unique or we can't create the dictionary because of duplicate keys. If we COULD create unique values we could do that, but there is no way to create unique values for arbitrary types, so we fall back to using the key/value pairs from the original dictionary. This is awkward because when the user writes dictionary.Values they only want the values, but we still need the key values under the covers.

I chose two serializers because I didn't want all that nastiness for the keys collection where we don't need it.

@rstam rstam requested a review from sanych-sun November 10, 2025 18:10
{
var keyType = keySerializer.ValueType;
var valueType = valueSerializer.ValueType;
var serializerType = typeof(DictionaryKeyCollectionSerializer<,>).MakeGenericType(keyType, valueType);
Copy link
Contributor

Choose a reason for hiding this comment

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

there's extra space between the variable name and "=" on this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

{
var keyType = keySerializer.ValueType;
var valueType = valueSerializer.ValueType;
var serializerType = typeof(DictionaryValueCollectionSerializer<,>).MakeGenericType(keyType, valueType);
Copy link
Contributor

Choose a reason for hiding this comment

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

there's extra space between the variable name and "=" on this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

public static IBsonSerializer Create(IBsonSerializer itemSerializer)
{
var itemType = itemSerializer.ValueType;
var serializerType = typeof(ICollectionSerializer<>).MakeGenericType(itemType);
Copy link
Contributor

Choose a reason for hiding this comment

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

there's extra space between the variable name and "=" on this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

{
var keyType = keySerializer.ValueType;
var valueType = valueSerializer.ValueType;
var serializerType = typeof(KeyValuePairWrappedValueSerializer<,>).MakeGenericType(keyType, valueType);
Copy link
Contributor

Choose a reason for hiding this comment

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

there's extra space between the variable name and "=" on this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add tests for calling Keys and Values on empty dictionaries?
Also maybe tests for calling Values on nested dictionaries -> Dictionary<string, Dictionary<string, int>>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. I will add more tests in a future commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

adelinowona
adelinowona previously approved these changes Nov 11, 2025
Copy link
Contributor

@adelinowona adelinowona left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@adelinowona adelinowona left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Member

@sanych-sun sanych-sun left a comment

Choose a reason for hiding this comment

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

LGTM

@rstam rstam merged commit 2c6ad30 into mongodb:main Nov 12, 2025
35 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Optimizations or refactoring (no new features or fixes).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants