-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
Conversation
…in an Update with Aggregation Pipeline
…assMapSerializer.
/// <summary> | ||
/// An interface implemented by BsonClassMapSerializer. | ||
/// </summary> | ||
public interface IBsonClassMapSerializer |
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 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) |
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.
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.
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.
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.
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 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.
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.
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) |
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.
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); |
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.
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); |
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.
Note that we are telling Translate
that the resultSerializer
must be memberSerializer
.
items.Add(itemTranslation.Ast); | ||
itemSerializer ??= itemTranslation.Serializer; | ||
if (itemSerializer == null) |
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.
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) |
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.
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); |
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 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?
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 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)
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. 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, |
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.
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); |
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 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() |
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 test still fails. Maybe we could discuss solutions.
I suspect we need some combination of the following to make this work:
- A way of asking a serializer for
T
what serializer to use for a base classB
or a derived classD
- A way of checking that the serializer of the translated value is "compatible" with the result serializer
The second almost surely requires the first.
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 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.
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) |
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 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; |
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.
Isn't the resultSerializer
here essentially memberSerializer
? No need to get the serializationInfo.Serializer
again
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.
True.
case ExpressionType.NewArrayInit: | ||
return NewArrayInitExpressionToAggregationExpressionTranslator.Translate(context, (NewArrayExpression)expression); | ||
return NewArrayInitExpressionToAggregationExpressionTranslator.Translate(context, (NewArrayExpression)expression, resultSerializer); |
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 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)
ExpressionTranslationOptions translationOptions, | ||
SymbolTable symbolTable, | ||
NameGenerator nameGenerator, | ||
ExpressionTranslationOptions translationOptions, | ||
TranslationContextData data = null) |
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.
Is there a special reason for moving the translationOptions parameter here?
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 |
No description provided.