-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CSHARP-5779: Support Dictionary Keys and Values properties #1807
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
Conversation
| return new AstComputedDocumentExpression(fields); | ||
| } | ||
|
|
||
| public static AstExpression ComputedDocument(IEnumerable<(string Name, AstExpression Value)> fields) |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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>> |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) | ||
| { |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>.
| case "Keys": | ||
| var keysAst = dictionaryRepresentation switch | ||
| { | ||
| DictionaryRepresentation.ArrayOfDocuments => AstExpression.Map(containerAst, kvpVar, AstExpression.GetField(kvpVar, "k")), |
There was a problem hiding this comment.
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.
There was a problem hiding this 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
KeysandValuesproperties - 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 } }"); |
Copilot
AI
Nov 10, 2025
There was a problem hiding this comment.
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.
| 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 } }"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed.
There was a problem hiding this comment.
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)), |
Copilot
AI
Nov 10, 2025
There was a problem hiding this comment.
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.
| DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstComputedArrayExpression.ArrayElemAt(kvpVar, 1)), | |
| DictionaryRepresentation.ArrayOfArrays => AstExpression.Map(containerAst, kvpVar, AstExpression.ArrayElemAt(kvpVar, 1)), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| { | ||
| var keyType = keySerializer.ValueType; | ||
| var valueType = valueSerializer.ValueType; | ||
| var serializerType = typeof(DictionaryKeyCollectionSerializer<,>).MakeGenericType(keyType, valueType); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
There was a problem hiding this comment.
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>>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
adelinowona
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
adelinowona
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
sanych-sun
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
No description provided.