CSHARP-5959: Replace switch by method name in SerializerFinderVisitMethodCall with lookup table with MethodInfo as a key#1961
Conversation
| } | ||
| } | ||
|
|
||
| // TODO: merge this and next methods. |
There was a problem hiding this comment.
I'll create a ticket for this, as these 2 methods looks like doing the same stuff.
There was a problem hiding this comment.
Pull request overview
This PR refactors LINQ3 serializer deduction for MethodCallExpression nodes by replacing a large name-based switch with a MethodInfo-keyed lookup table (plus a small dynamic fallback) to improve maintainability and correctness around overload resolution.
Changes:
- Replaced
switch (node.Method.Name)inSerializerFinderVisitor.VisitMethodCallwith aMethodInfo -> deducerregistry (including caching and generic-method normalization). - Added new
StringMethodreflection entries forstring.Equalsinstance/static variants and updated registrations accordingly. - Introduced
EnumerableMethod.IsContainsMethod(MethodInfo)to support dynamic detection ofContainsmethods.
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderVisitMethodCall.cs |
Major refactor: method serializer deduction now uses a MethodInfo lookup table with dynamic fallback and caching. |
src/MongoDB.Driver/Linq/Linq3Implementation/SerializerFinders/SerializerFinderHelperMethods.cs |
Adds a TODO comment around helper-method duplication. |
src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/StringMethod.cs |
Adds reflection MethodInfos for string.Equals (instance + static) and exposes them. |
src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/EnumerableOrQueryableMethod.cs |
Removes the AnyOverloads set (now uses Any/AnyWithPredicate sets directly). |
src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/EnumerableMethod.cs |
Adds IsContainsMethod(MethodInfo) to support dynamic Contains deduction. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| void DeduceCompareOrCompareToMethodSerializers() | ||
| // DeduceAddToSetMethodSerializers |
There was a problem hiding this comment.
I've left such comments so for now, for review purposes. So it would be easier to find where the implementation came from. I'll remove them before merging.
|
|
||
| void DeduceCompareOrCompareToMethodSerializers() | ||
| // DeduceAddToSetMethodSerializers | ||
| private static void DeduceAddToSetMethodSerializers(SerializerFinderVisitor visitor, MethodCallExpression expression) |
There was a problem hiding this comment.
I've moved all deducers from local method to static-class level methods, because codecov is struggling to show the code coverage for local methods. As the result I can see which deducers are not covered by tests. Probably I need to create more test cases to improve code coverage.
| var method = node.Method; | ||
| var arguments = node.Arguments; | ||
| var methodInfo = node.Method.IsGenericMethod && !node.Method.ContainsGenericParameters ? node.Method.GetGenericMethodDefinition() : node.Method; | ||
| var deducer = MethodCallSerializerDeducers.GetSerializerDeducer(methodInfo); |
There was a problem hiding this comment.
I've decided to move all deducers implementation into private static class simply to clear the "entry" point, to have this method on the very top and visible.
| RegisterSerializerDeducers(EnumerableOrQueryableMethod.ElementAtOverloads, (visitor, expression) => visitor.DeduceItemAndCollectionSerializers(expression, expression.Arguments[0])); | ||
| RegisterSerializerDeducers(EnumerableOrQueryableMethod.Except, DeduceExceptMethodSerializers); | ||
| // TODO: Definitely wrong registration copy-pasted from prev code, investigate before merge to main | ||
| // RegisterSerializerResolver(EnumerableMethod.First, DeduceSetWindowFieldsMethodSerializers); |
There was a problem hiding this comment.
I'm about to investigate this problem. Will fix or remove the registration before merging the PR.
There was a problem hiding this comment.
Don't forget about this!
| RegisterSerializerDeducers(EnumerableOrQueryableMethod.ElementAtOverloads, (visitor, expression) => visitor.DeduceItemAndCollectionSerializers(expression, expression.Arguments[0])); | ||
| RegisterSerializerDeducers(EnumerableOrQueryableMethod.Except, DeduceExceptMethodSerializers); | ||
| // TODO: Definitely wrong registration copy-pasted from prev code, investigate before merge to main | ||
| // RegisterSerializerResolver(EnumerableMethod.First, DeduceSetWindowFieldsMethodSerializers); |
There was a problem hiding this comment.
Don't forget about this!
| private static class MethodCallSerializerDeducers | ||
| { | ||
| private static readonly Dictionary<MethodInfo, Action<SerializerFinderVisitor, MethodCallExpression>> __serializerResolvers = new(); | ||
| private static readonly ReaderWriterLockSlim __serializerResolversLock = new(); |
There was a problem hiding this comment.
Why do we not use a ConcurrentDictionary instead of the whole read-lock / upgrade-to-write / recheck dance in GetSerializerDeducer?
There was a problem hiding this comment.
Will do. Somehow I was thinking having Dictionary<> for "mostly readonly" access might be better, but apparently ConcurrentDictionary also handles this use case better then combination of Dictionary<> and ReadWriteLockSlim.
Thank you for the comment!
There was a problem hiding this comment.
Done, however instead of simply use the GetOrAdd method, I've decided to use TryGetValue, then manually call CreateDynamicSerializerDeducer and if it returns not-null then use GetOrAdd. We need this to avoid caching of null values in the serializer deducers dictionary.
|
Looks like a reasonable change to me. |
| public static IReadOnlyMethodInfoSet ReverseOverloads => __reverseOverloads; | ||
|
|
||
| // public methods | ||
| public static bool IsContainsMethod(MethodInfo method) |
There was a problem hiding this comment.
Is this going to also match String.Contains? A lot of providers typically special case excluding String.Contains as we don't want to handle looking for chars in a string in the same way we look for types in a containable list.
There was a problem hiding this comment.
Nope, String.Contains has it's pre-registered deducer (see like 216).
| [TestHelpers.MakeLambda((MyModel model) => model.Value.Equals(1f)), typeof(BooleanSerializer)], | ||
| [TestHelpers.MakeLambda((MyModel model) => model.Value.Equals(model.Other)), typeof(BooleanSerializer)], | ||
| [TestHelpers.MakeLambda((MyModel model) => float.Parse("42")), typeof(SingleSerializer)], | ||
| [TestHelpers.MakeLambda((MyModel model) => float.Parse(model.StringValue)), typeof(SingleSerializer)], |
There was a problem hiding this comment.
42 probably should have fractial parts for the float/decimal/double tests originally?
There was a problem hiding this comment.
We need non-constant here, so the PartialEvaluator will not replace this with ConstantExpression.
| static void DeduceToArrayMethodSerializers(SerializerFinderVisitor visitor, MethodCallExpression expression) | ||
| { | ||
| DeduceUnknownMethodSerializer(); | ||
| var sourceExpression = expression.Method.IsStatic ? expression.Arguments[0] : expression.Object; | ||
| visitor.DeduceCollectionAndCollectionSerializers(expression, sourceExpression); | ||
| } | ||
| } | ||
|
|
||
| bool IsDictionaryContainsKeyExpression(out Expression keyExpression) | ||
| { | ||
| if (DictionaryMethod.IsContainsKeyMethod(method)) | ||
| static void DeduceToListSerializers(SerializerFinderVisitor visitor, MethodCallExpression expression) | ||
| { | ||
| keyExpression = arguments[0]; | ||
| return true; | ||
| if (visitor.IsNotKnown(expression)) | ||
| { | ||
| var source = expression.Method.IsStatic ? expression.Arguments[0] : expression.Object; | ||
| if (visitor.IsKnown(source, out var sourceSerializer)) | ||
| { | ||
| var sourceItemSerializer = ArraySerializerHelper.GetItemSerializer(sourceSerializer); | ||
| var resultSerializer = ListSerializer.Create(sourceItemSerializer); | ||
| visitor.AddNodeSerializer(expression, resultSerializer); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Why are DeduceToArray and DeduceToList so different? Shouldn't they have similar bodies?
There was a problem hiding this comment.
It's an interesting question, but it's out of this PR scope. I'll double-check if the method was migrated/moved properly and can create a separate ticket for the investigation if you want.
| "ToList" => DeduceToListSerializers, | ||
| "ToString" => (visitor, expression) => visitor.DeduceSerializer(expression, StringSerializer.Instance), |
There was a problem hiding this comment.
why don't these have guards like the others?
There was a problem hiding this comment.
Do you mean checking if node is known already? Because DeduceSerializer is doing that for us + we do not create any new serializers here, but passing pre-created one.
| { | ||
| DeduceUnknownMethodSerializer(); | ||
| return null; |
There was a problem hiding this comment.
I believe the old code was caching this null case. We don't need to do that anymore?
…thodCall with lookup table with MethodInfo as a key
The idea of the PR is to replace the switch by string method's name and checking of methodInfo, with the dictionary of deducers by method info. Most of the use-cases is covered by "static" registration, however there are number of use-cases where we duck-typing the method call (see CreateDynamicSerializerDeducer method), in such case we do dynamic registration as we go: so the next translation of the method will use "previously registered deducer" and do not need to go through all checks.