CSHARP-4443: Add comprehensive dictionary LINQ support for all 3 representations#1804
CSHARP-4443: Add comprehensive dictionary LINQ support for all 3 representations#1804adelinowona wants to merge 15 commits intomongodb:mainfrom
Conversation
rstam
left a comment
There was a problem hiding this comment.
Note: I've only reviewed the tests, where I have a lot of suggestions about possible MQL for things you didn't implement or "better" MQL in some cases for things you did implement.
Please take all suggestions with a grain of salt. You may have reasons for thinking that what I think is "better" MQL might not actually be better.
I am postponing reviewing the implementation because if you accept many of the MQL suggestions you will need to make many changes to the implementation.
I will review the implementation after you have accepted/rejected the MQL suggestions.
|
|
||
| namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira | ||
| { | ||
| public class CSharp2509Tests : LinqIntegrationTest<CSharp2509Tests.ClassFixture> |
There was a problem hiding this comment.
I was surprised that these tests were deleted, but Adelin says they duplicate new tests he added in CSharp4443Tests.cs.
We don't usually remove tests... why bother?
There was a problem hiding this comment.
Note to other reviewers: we have to manually verify that we haven't lost any test coverage as a result of deleting these tests.
tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs
Outdated
Show resolved
Hide resolved
tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs
Outdated
Show resolved
Hide resolved
tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs
Outdated
Show resolved
Hide resolved
tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs
Outdated
Show resolved
Hide resolved
tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs
Outdated
Show resolved
Hide resolved
tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs
Outdated
Show resolved
Hide resolved
tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs
Outdated
Show resolved
Hide resolved
tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs
Outdated
Show resolved
Hide resolved
tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4443Tests.cs
Outdated
Show resolved
Hide resolved
34f74ce to
fdb6fa7
Compare
There was a problem hiding this comment.
Pull request overview
This PR extends LINQ support for dictionary operations across all three BSON representations (Document, ArrayOfDocuments, and ArrayOfArrays). Previously, some operations like ContainsKey, ContainsValue, indexer access, and property access (Count, Keys, Values) only worked with the Document representation.
Key Changes:
- Extended
ContainsKeyandContainsValueto support ArrayOfDocuments and ArrayOfArrays representations - Added support for dictionary indexer access across all representations
- Implemented dictionary property access (Count, Keys, Values) for all representations
- Added support for KeyValuePair property access in array representation
- Removed obsolete test files that tested the previous limited functionality
Reviewed changes
Copilot reviewed 24 out of 25 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
CSharp4813Tests.cs |
Removed dictionary-specific test methods that tested unsupported scenarios |
CSharp4557Tests.cs |
Deleted entire test file for ContainsKey with Document representation only |
CSharp2509Tests.cs |
Deleted entire test file for ContainsValue with limited representation support |
MemberExpressionToFilterFieldTranslator.cs |
Added KeyValuePair member translation and validation for Dictionary collections |
ContainsValueMethodToFilterTranslator.cs |
Extended to support ArrayOfArrays representation |
ContainsMethodToFilterTranslator.cs |
Added dictionary Keys/Values Contains support |
ContainsKeyMethodToFilterTranslator.cs |
Extended to support ArrayOfDocuments and ArrayOfArrays representations |
AllOrAnyMethodToFilterTranslator.cs |
Added validation to reject All/Any on Document representation dictionaries |
GetItemMethodToAggregationExpressionTranslator.cs |
Implemented indexer access for all dictionary representations |
ContainsValueMethodToAggregationExpressionTranslator.cs |
Refactored to extract reusable translation method |
ContainsMethodToAggregationExpressionTranslator.cs |
Added dictionary Keys/Values Contains support in aggregation expressions |
ContainsKeyMethodToAggregationExpressionTranslator.cs |
Extended to support all dictionary representations |
MemberExpressionToAggregationExpressionTranslator.cs |
Added comprehensive dictionary and KeyValuePair property translation |
AstExpression.cs |
Added ArrayToObject helper method |
KeyValuePairSerializer.cs |
Added IKeyValuePairSerializerV2 interface with key/value serializer accessors |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
...ors/ExpressionToFilterTranslators/MethodTranslators/ContainsValueMethodToFilterTranslator.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Moved the tests on Keys and Values properties of Dictionaries into this file since I think the ticket csharp5779 should have tested them.
There was a problem hiding this comment.
Just a comment that resorting the entire file made a diff difficult.
rstam
left a comment
There was a problem hiding this comment.
Overall looking great. I would characterize most of my comments or requested changes as minor.
I haven't had time to review the tests yet, but wanted to give you the feedback I have so far.
Good work!
| else | ||
| { | ||
| ast = AstExpression.Avg(sourceTranslation.Ast); | ||
| var sourceItemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer); |
There was a problem hiding this comment.
There is similar code in the translators for Max, Min and Sum.
Is there any way to share the code?
No problem if not.
There was a problem hiding this comment.
Good point, added some code for sharing.
| Expression valueExpression, | ||
| out TranslatedExpression translation) | ||
| { | ||
| translation = null; |
There was a problem hiding this comment.
Same comment as above about assigning null to out parameter at top of method.
|
|
||
| if (!declaringType.IsGenericType || | ||
| (declaringType.GetGenericTypeDefinition() != typeof(Dictionary<,>) && | ||
| declaringType.GetGenericTypeDefinition() != typeof(IDictionary<,>))) |
There was a problem hiding this comment.
110-112 might be simpler like this:
if (!declaringType.ImplementsDictionaryInterface(out var keyType, out var valueType))
also: test with IReadOnlyDictionary
There was a problem hiding this comment.
Good suggestion! After investigating, I think we can actually remove the filter entirely (lines 110-115).
The filter was originally meant to match what MemberExpressionTranslator supports for .Keys and .Values properties. However, in ContainsMethodToAggregationExpressionTranslator, we don't actually translate
those members - we just use them as a pattern match to identify that we should use ContainsKey/ContainsValue translation. That is, dictionary.Keys.Contains(Key) should have the same translation as dictionary.ContainsKey(key).
So we can remove the declaring type check entirely. This automatically enables support for IReadOnlyDictionary<,>, SortedDictionary<,>, and any other dictionary type with a proper serializer.
Does that sound reasonable?
|
|
||
| private static bool TryTranslateKeyValuePairProperty(MemberExpression expression, TranslatedExpression container, MemberInfo memberInfo, out TranslatedExpression result) | ||
| { | ||
| result = null; |
There was a problem hiding this comment.
I don't like assigning null to out variables at the beginning of the method.
I realize it makes it more convenient to return false, but it also disables the compiler warning you get if you try to return true without setting an out parameter.
Your call.
There was a problem hiding this comment.
That's fair, I updated it.
|
|
||
| if (container.Expression.Type.IsGenericType && | ||
| container.Expression.Type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>) && | ||
| container.Serializer is IKeyValuePairSerializerV2 { Representation: BsonType.Array } kvpSerializer) |
There was a problem hiding this comment.
Why are we ignoring Document representation here?
There was a problem hiding this comment.
We're only handling Array representation in TryTranslateKeyValuePairProperty because Document representation is already correctly handled by the general member translation logic that follows (lines
94-108).
That code uses GetMemberSerializationInfo which returns the correct field names ("k" and "v") and serializers for Document-represented KeyValuePairs.
The method is only called when !AreMembersRepresentedAsFields (line 71), which is true for Array representation but false for Document representation.
|
|
||
| if (fieldTranslation.Serializer is IBsonDictionarySerializer { DictionaryRepresentation: DictionaryRepresentation.Document }) | ||
| { | ||
| throw new ExpressionNotSupportedException(expression); |
There was a problem hiding this comment.
throw new ExpressionNotSupportedException(expression, because: "dictionary is represented as a document");
Add because?
| case DictionaryRepresentation.ArrayOfDocuments: | ||
| case DictionaryRepresentation.ArrayOfArrays: | ||
| { | ||
| var key = GetKeyStringConstant(expression, keyExpression, dictionarySerializer.KeySerializer); |
There was a problem hiding this comment.
If you move setting of key variable before the switch the braces would not be needed.
| { | ||
| case "Keys": | ||
| { | ||
| var dictionaryExpression = memberExpression.Expression; |
There was a problem hiding this comment.
If you move setting of dictionaryExpression variable before the switch the braces would not be needed.
|
|
||
| if (!declaringType.IsGenericType || | ||
| (declaringType.GetGenericTypeDefinition() != typeof(Dictionary<,>) && | ||
| declaringType.GetGenericTypeDefinition() != typeof(IDictionary<,>))) |
There was a problem hiding this comment.
This would probably be simpler:
if (!declaringType.ImplementsDictionaryInterface(out _, out _))
and test with IReadOnlyDictionary also?
| (memberExpression.Type.GetGenericTypeDefinition() == typeof(Dictionary<,>.KeyCollection) || | ||
| memberExpression.Type.GetGenericTypeDefinition() == typeof(Dictionary<,>.ValueCollection))) | ||
| { | ||
| throw new ExpressionNotSupportedException(memberExpression); |
| public CSharp5779Tests(ClassFixture fixture) | ||
| : base(fixture) | ||
| { | ||
| } | ||
|
|
||
| [Fact] | ||
| public void DictionaryAsArrayOfArrays_Keys_should_work() |
There was a problem hiding this comment.
Shouldn't you have kept this test along with the two new ones with Contains and Count?
There was a problem hiding this comment.
Oh wait.... did you just move the old test in a way that makes the differ harder to do?
There was a problem hiding this comment.
Yes, the test is there, I just moved it around when I sorted the file.
There was a problem hiding this comment.
Just a comment that resorting the entire file made a diff difficult.
| .Select(x => x.DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays.Values.SelectMany(n => n.Keys)); | ||
|
|
||
| var stages = Translate(collection, queryable); | ||
| AssertStages(stages, "{ $project : { _v : { $reduce : { input : { $map : { input : { $objectToArray : '$DictionaryAsDocumentOfNestedDictionaryAsArrayOfArrays' }, as : 'n', in : { $map : { input : '$$n.v', as : 'kvp', in : { $arrayElemAt : ['$$kvp', 0] } } } } }, initialValue : [], in : { $concatArrays : ['$$value', '$$this'] } } }, _id : 0 } }"); |
There was a problem hiding this comment.
Replace double spaces with single space.
There was a problem hiding this comment.
I don't see any double spaces in this area of the code?
There was a problem hiding this comment.
Look for them in the string constant.
- Extract common IWrappedValueSerializer unwrapping logic into helper
- Add ExpressionToAggregationExpressionTranslatorHelper.CreateAggregationAstWithUnwrapping
- Refactor Average, Sum, Max/Min translators to use shared helper
- Improve error messages with 'because' parameter
- CountComparisonExpressionToFilterTranslator: clarify size must be integer constant
- MemberExpressionToFilterFieldTranslator: explain KeyCollection/ValueCollection limitation
- Fix code style issues
- Move out parameter initialization after if blocks for compiler safety (TryTranslateKeyValuePairProperty)
- Fix indentation in DictionaryIndexerComparisonExpressionToFilterTranslator
- Hoist common variables before switch statements (ContainsKey, Contains translators)
- Remove Count <= 0 pattern support (Count is never negative)
rstam
left a comment
There was a problem hiding this comment.
I've reviewed just the changes from the last commit I looked at to the most recent commit.
Can't remember if I need to do a quick full review of the whole PR.
In the meantime, I have one small change to suggest.
| { | ||
| return AstExpression.Size(mapExpression.Input); | ||
| } | ||
|
|
There was a problem hiding this comment.
I think this simplification could be applied more generally:
// { $size : { map : { input : <array>, as : <var> in : <expr> } } } => { $size : <array> }
if (node.Operator == AstUnaryOperator.Size &&
arg is AstMapExpression mapExpression)
{
return AstExpression.Size(mapExpression.Input);
}
|
I did a quick full review of all the changes in all the comments and I think the only comment pending on my part is the recent one about generalizing the |
No description provided.