Skip to content
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

CSHARP-5435: NotSupportedException when using Set with sub-documents in an Update with Aggregation Pipeline. #1590

Closed
wants to merge 8 commits into from

Conversation

rstam
Copy link
Contributor

@rstam rstam commented Jan 10, 2025

No description provided.

@rstam rstam requested a review from a team as a code owner January 10, 2025 17:22
/// <summary>
/// An interface implemented by BsonClassMapSerializer.
/// </summary>
public interface IBsonClassMapSerializer
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 non-generic interface makes it possible for the LINQ translator to get the ClassMap without using reflection.

@@ -27,7 +27,7 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggreg
internal static class ExpressionToAggregationExpressionTranslator
{
// public static methods
public static AggregationExpression Translate(TranslationContext context, Expression expression)
public static AggregationExpression Translate(TranslationContext context, Expression expression, IBsonSerializer resultSerializer = null)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the caller passes in a resultSerializer it means that the translation MUST use that translator.

If the caller passes in null then the result can use whatever serializer comes out from the translation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Making the resultSerializer argument optional reduced the number of changes required.

Let me know if you think it should be mandatory, but then every single call of this method would need to be touched.

Copy link
Contributor

Choose a reason for hiding this comment

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

This approach makes sense to me. Making resultSerializer optional with this behavior provides good flexibility while maintaining compatibility and avoiding creating unnecessary churn by forcing updates to all existing call sites, when many of them likely don't need to override the default serialization behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alex and I are discussing whether to make a new ticket for this work, something like "add targetSerializer parameter to Translate methods to pass down expected serializer for the result".

A separate ticket makes sense to me because this is a much broader issue than just Update.Set.

I also think we should make the targetSerializer parameter mandatory, even though it will require a lot of work. That way we can find all the places where it needs to be passed down (you mentioned in another comment that we might have missed some enumerable cases, so that's an example of something we might find by making the parameter mandatory).

{
if (resultSerializer != null)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Only use the new code if the caller told us we had to use a particular resultSerializer.

Using the existing code if not preserves maximum backward compatibility.

var argumentExpression = constructorArguments[i];
var memberName = creatorMapArguments?[i].Name; // null if there is no classMap

var (elementName, memberSerializer) = FindMemberElementNameAndSerializer(argumentExpression, classMap, memberName, documentSerializer, parameterName);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

FindMemberElementNameAndSerializer will prioritize using the classMap and memberName if they are available, otherwise it will fallback to trying to match constructor parameter names to member names.

throw new ExpressionNotSupportedException(expression, because: $"couldn't find matching class member for constructor parameter {parameterName}");
}

var argumentTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, argumentExpression, memberSerializer);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that we are telling Translate that the resultSerializer must be memberSerializer.

items.Add(itemTranslation.Ast);
itemSerializer ??= itemTranslation.Serializer;
if (itemSerializer == null)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the caller did not provide a resultSerializer then itemSerializer will be initialized to the serializer of the first translated item in the array.

itemSerializer ??= BsonSerializer.LookupSerializer(itemType); // if the array is empty itemSerializer will be null
var arraySerializerType = typeof(ArraySerializer<>).MakeGenericType(itemType);
var arraySerializer = (IBsonSerializer)Activator.CreateInstance(arraySerializerType, itemSerializer);
if (arraySerializer == null)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If the caller did not provide a resultSerializer we create one now.

Note: arraySerializer and resultSerializer are the same thing just that arraySerializer is declared IBsonArraySerializer instead of just IBsonSerializer.

case ExpressionType.NewArrayInit:
return NewArrayInitExpressionToAggregationExpressionTranslator.Translate(context, (NewArrayExpression)expression);
return NewArrayInitExpressionToAggregationExpressionTranslator.Translate(context, (NewArrayExpression)expression, resultSerializer);
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 to do some more investigation to see where else we might need to pass the resultSerializer.

For example, we currently handle T[], but what about List<T> or other enumerable types?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think if we are going to handle T[], then for consistency we should probably support other collection types. We might need to pass the resultSerializer as well in NewListExpressionToAggregationExpressionTranslator, NewHashSetExpressionToAggregationExpressionTranslator (those are the ones I could find so far dealing with enumerable types)

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. I think we are missing more scenarios where the targetSerializer need to passed down.

Note: I'm renaming what I used to call resultSerializer to targetSerializer because "result" sounds more like an output than an input.


namespace MongoDB.Driver.Linq.Linq3Implementation.Translators
{
internal class TranslationContext
{
#region static
public static TranslationContext Create(
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.

Apparently we just forgot to remove this unused parameter before now.

@@ -153,7 +153,8 @@ private static AstComputedField CreateComputedField(TranslationContext context,
}
else
{
var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression);
var resultSerializer = serializationInfo.Serializer;
var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression, resultSerializer);
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 the entry point at which translation of Update.Set tells the Translate method what resultSerializer to use.

From here the resultSerializer is propagated down to where it is needed.

}

[Fact]
public void Test_set_ValueObject_to_derived_value_using_property_setter()
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 test still fails. Maybe we could discuss solutions.

I suspect we need some combination of the following to make this work:

  1. A way of asking a serializer for T what serializer to use for a base class B or a derived class D
  2. A way of checking that the serializer of the translated value is "compatible" with the result serializer

The second almost surely requires the first.

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 issue is still pending. I haven't solved it yet. I'm not sure it can be solved without adding new functionality to polymorphic serializers to ask for a new serializer for a base or derived type.

@rstam
Copy link
Contributor Author

rstam commented Jan 10, 2025

All the variants are failing because we still need to figure out how to handle setting the property to a derived value.

@@ -27,7 +27,7 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggreg
internal static class ExpressionToAggregationExpressionTranslator
{
// public static methods
public static AggregationExpression Translate(TranslationContext context, Expression expression)
public static AggregationExpression Translate(TranslationContext context, Expression expression, IBsonSerializer resultSerializer = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

This approach makes sense to me. Making resultSerializer optional with this behavior provides good flexibility while maintaining compatibility and avoiding creating unnecessary churn by forcing updates to all existing call sites, when many of them likely don't need to override the default serialization behavior.

@@ -153,7 +153,8 @@ private static AstComputedField CreateComputedField(TranslationContext context,
}
else
{
var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression);
var resultSerializer = serializationInfo.Serializer;
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't the resultSerializer here essentially memberSerializer? No need to get the serializationInfo.Serializer again

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True.

case ExpressionType.NewArrayInit:
return NewArrayInitExpressionToAggregationExpressionTranslator.Translate(context, (NewArrayExpression)expression);
return NewArrayInitExpressionToAggregationExpressionTranslator.Translate(context, (NewArrayExpression)expression, resultSerializer);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think if we are going to handle T[], then for consistency we should probably support other collection types. We might need to pass the resultSerializer as well in NewListExpressionToAggregationExpressionTranslator, NewHashSetExpressionToAggregationExpressionTranslator (those are the ones I could find so far dealing with enumerable types)

Comment on lines +47 to 50
ExpressionTranslationOptions translationOptions,
SymbolTable symbolTable,
NameGenerator nameGenerator,
ExpressionTranslationOptions translationOptions,
TranslationContextData data = null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a special reason for moving the translationOptions parameter here?

@rstam
Copy link
Contributor Author

rstam commented Jan 16, 2025

Because the work is broader than just Update.Set a new JIRA ticket CSHARP-5461 has been created. Update.Set is just one manifestation of the broader problem.

This PR is replaced by this one: #1596

@rstam rstam closed this Jan 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants