Skip to content

CSHARP-5959: Replace switch by method name in SerializerFinderVisitMethodCall with lookup table with MethodInfo as a key#1961

Draft
sanych-sun wants to merge 6 commits into
mongodb:mainfrom
sanych-sun:csharp5959
Draft

CSHARP-5959: Replace switch by method name in SerializerFinderVisitMethodCall with lookup table with MethodInfo as a key#1961
sanych-sun wants to merge 6 commits into
mongodb:mainfrom
sanych-sun:csharp5959

Conversation

@sanych-sun
Copy link
Copy Markdown
Member

@sanych-sun sanych-sun commented Apr 22, 2026

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.

Copilot AI review requested due to automatic review settings April 22, 2026 21:09
@sanych-sun sanych-sun requested a review from a team as a code owner April 22, 2026 21:09
@sanych-sun sanych-sun requested a review from ajcvickers April 22, 2026 21:09
}
}

// TODO: merge this and next methods.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'll create a ticket for this, as these 2 methods looks like doing the same stuff.

@sanych-sun sanych-sun requested a review from adelinowona April 22, 2026 21:11
Copy link
Copy Markdown
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 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) in SerializerFinderVisitor.VisitMethodCall with a MethodInfo -> deducer registry (including caching and generic-method normalization).
  • Added new StringMethod reflection entries for string.Equals instance/static variants and updated registrations accordingly.
  • Introduced EnumerableMethod.IsContainsMethod(MethodInfo) to support dynamic detection of Contains methods.

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
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm about to investigate this problem. Will fix or remove the registration before merging the PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Don't forget about this!

@sanych-sun sanych-sun added the improvement Optimizations or refactoring (no new features or fixes). label Apr 22, 2026
@sanych-sun sanych-sun marked this pull request as draft April 22, 2026 22:33
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we not use a ConcurrentDictionary instead of the whole read-lock / upgrade-to-write / recheck dance in GetSerializerDeducer?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

@ajcvickers
Copy link
Copy Markdown
Contributor

Looks like a reasonable change to me.

public static IReadOnlyMethodInfoSet ReverseOverloads => __reverseOverloads;

// public methods
public static bool IsContainsMethod(MethodInfo method)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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)],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

42 probably should have fractial parts for the float/decimal/double tests originally?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We need non-constant here, so the PartialEvaluator will not replace this with ConstantExpression.

Comment on lines +1759 to 1777
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);
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why are DeduceToArray and DeduceToList so different? Shouldn't they have similar bodies?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment on lines +1561 to +1562
"ToList" => DeduceToListSerializers,
"ToString" => (visitor, expression) => visitor.DeduceSerializer(expression, StringSerializer.Instance),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why don't these have guards like the others?

Copy link
Copy Markdown
Member Author

@sanych-sun sanych-sun Apr 28, 2026

Choose a reason for hiding this comment

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

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.

@sanych-sun sanych-sun requested a review from adelinowona April 28, 2026 20:44
{
DeduceUnknownMethodSerializer();
return null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I believe the old code was caching this null case. We don't need to do that anymore?

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.

5 participants