-
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
Changes from all commits
d5e3140
6afb529
55e1883
a5dcc52
5d06631
e0d3f5d
0d8369d
314be7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. If the caller passes in a If the caller passes in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Making the 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 commentThe reason will be displayed to describe this comment to others. Learn more. This approach makes sense to me. Making There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 A separate ticket makes sense to me because this is a much broader issue than just I also think we should make the |
||
{ | ||
switch (expression.NodeType) | ||
{ | ||
|
@@ -67,21 +67,21 @@ public static AggregationExpression Translate(TranslationContext context, Expres | |
case ExpressionType.Conditional: | ||
return ConditionalExpressionToAggregationExpressionTranslator.Translate(context, (ConditionalExpression)expression); | ||
case ExpressionType.Constant: | ||
return ConstantExpressionToAggregationExpressionTranslator.Translate((ConstantExpression)expression); | ||
return ConstantExpressionToAggregationExpressionTranslator.Translate((ConstantExpression)expression, resultSerializer); | ||
case ExpressionType.Index: | ||
return IndexExpressionToAggregationExpressionTranslator.Translate(context, (IndexExpression)expression); | ||
case ExpressionType.ListInit: | ||
return ListInitExpressionToAggregationExpressionTranslator.Translate(context, (ListInitExpression)expression); | ||
case ExpressionType.MemberAccess: | ||
return MemberExpressionToAggregationExpressionTranslator.Translate(context, (MemberExpression)expression); | ||
case ExpressionType.MemberInit: | ||
return MemberInitExpressionToAggregationExpressionTranslator.Translate(context, (MemberInitExpression)expression); | ||
return MemberInitExpressionToAggregationExpressionTranslator.Translate(context, (MemberInitExpression)expression, resultSerializer); | ||
case ExpressionType.Negate: | ||
return NegateExpressionToAggregationExpressionTranslator.Translate(context, (UnaryExpression)expression); | ||
case ExpressionType.New: | ||
return NewExpressionToAggregationExpressionTranslator.Translate(context, (NewExpression)expression); | ||
return NewExpressionToAggregationExpressionTranslator.Translate(context, (NewExpression)expression, resultSerializer); | ||
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 commentThe 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 For example, we currently handle There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we are going to handle There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. I think we are missing more scenarios where the Note: I'm renaming what I used to call |
||
case ExpressionType.Parameter: | ||
return ParameterExpressionToAggregationExpressionTranslator.Translate(context, (ParameterExpression)expression); | ||
case ExpressionType.TypeIs: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,22 +28,28 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggreg | |
{ | ||
internal static class MemberInitExpressionToAggregationExpressionTranslator | ||
{ | ||
public static AggregationExpression Translate(TranslationContext context, MemberInitExpression expression) | ||
public static AggregationExpression Translate(TranslationContext context, MemberInitExpression expression, IBsonSerializer resultSerializer) | ||
{ | ||
if (expression.Type == typeof(BsonDocument)) | ||
{ | ||
return NewBsonDocumentExpressionToAggregationExpressionTranslator.Translate(context, expression); | ||
} | ||
|
||
return Translate(context, expression, expression.NewExpression, expression.Bindings); | ||
return Translate(context, expression, expression.NewExpression, expression.Bindings, resultSerializer); | ||
} | ||
|
||
public static AggregationExpression Translate( | ||
TranslationContext context, | ||
Expression expression, | ||
NewExpression newExpression, | ||
IReadOnlyList<MemberBinding> bindings) | ||
IReadOnlyList<MemberBinding> bindings, | ||
IBsonSerializer resultSerializer) | ||
{ | ||
if (resultSerializer != null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Using the existing code if not preserves maximum backward compatibility. |
||
{ | ||
return TranslateWithTargetSerializer(context, expression, newExpression, bindings, resultSerializer); | ||
} | ||
|
||
var constructorInfo = newExpression.Constructor; // note: can be null when using the default constructor with a struct | ||
var constructorArguments = newExpression.Arguments; | ||
var computedFields = new List<AstComputedField>(); | ||
|
@@ -100,6 +106,71 @@ public static AggregationExpression Translate( | |
return new AggregationExpression(expression, ast, serializer); | ||
} | ||
|
||
private static AggregationExpression TranslateWithTargetSerializer( | ||
TranslationContext context, | ||
Expression expression, | ||
NewExpression newExpression, | ||
IReadOnlyList<MemberBinding> bindings, | ||
IBsonSerializer resultSerializer) | ||
{ | ||
if (!(resultSerializer is IBsonDocumentSerializer documentSerializer)) | ||
{ | ||
throw new ExpressionNotSupportedException(expression, because: $"serializer class {resultSerializer.GetType()} does not implement IBsonDocumentSerializer."); | ||
} | ||
|
||
var constructorInfo = newExpression.Constructor; // note: can be null when using the default constructor with a struct | ||
var constructorArguments = newExpression.Arguments; | ||
var computedFields = new List<AstComputedField>(); | ||
|
||
if (constructorInfo != null && constructorArguments.Count > 0) | ||
{ | ||
var constructorParameters = constructorInfo.GetParameters(); | ||
|
||
// if the documentSerializer is a BsonClassMappedSerializer we can use the classMap | ||
var classMap = (documentSerializer as IBsonClassMapSerializer)?.ClassMap; | ||
var creatorMap = classMap == null ? null : FindMatchingCreatorMap(classMap, constructorInfo); | ||
if (creatorMap == null && classMap != null) | ||
{ | ||
throw new ExpressionNotSupportedException(expression, because: "couldn't find matching creator map"); | ||
} | ||
var creatorMapArguments = creatorMap?.Arguments?.ToArray(); | ||
|
||
for (var i = 0; i < constructorParameters.Length; i++) | ||
{ | ||
var parameterName = constructorParameters[i].Name; | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
if (elementName == null) | ||
{ | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Note that we are telling |
||
computedFields.Add(AstExpression.ComputedField(elementName, argumentTranslation.Ast)); | ||
} | ||
} | ||
|
||
foreach (var binding in bindings) | ||
{ | ||
var memberAssignment = (MemberAssignment)binding; | ||
var member = memberAssignment.Member; | ||
var valueExpression = memberAssignment.Expression; | ||
if (!documentSerializer.TryGetMemberSerializationInfo(member.Name, out var memberSerializationInfo)) | ||
{ | ||
throw new ExpressionNotSupportedException(valueExpression, expression, because: $"couldn't find member {member.Name}"); | ||
} | ||
var memberSerializer = memberSerializationInfo.Serializer; | ||
|
||
var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression, memberSerializer); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note that we are telling |
||
computedFields.Add(AstExpression.ComputedField(memberSerializationInfo.ElementName, valueTranslation.Ast)); | ||
} | ||
|
||
var ast = AstExpression.ComputedDocument(computedFields); | ||
return new AggregationExpression(expression, ast, documentSerializer); | ||
} | ||
|
||
private static BsonClassMap CreateClassMap(Type classType, ConstructorInfo constructorInfo, out BsonCreatorMap creatorMap) | ||
{ | ||
BsonClassMap baseClassMap = null; | ||
|
@@ -190,6 +261,36 @@ private static void EnsureDefaultValue(BsonMemberMap memberMap) | |
memberMap.SetDefaultValue(defaultValue); | ||
} | ||
|
||
private static BsonCreatorMap FindMatchingCreatorMap(BsonClassMap classMap, ConstructorInfo constructorInfo) | ||
=> classMap?.CreatorMaps.FirstOrDefault(m => m.MemberInfo.Equals(constructorInfo)); | ||
|
||
private static (string, IBsonSerializer) FindMemberElementNameAndSerializer( | ||
Expression expression, | ||
BsonClassMap classMap, | ||
string memberName, | ||
IBsonDocumentSerializer documentSerializer, | ||
string constructorParameterName) | ||
{ | ||
// if we have a classMap use it | ||
if (classMap != null) | ||
{ | ||
var memberMap = FindMemberMap(expression, classMap, memberName); | ||
return (memberMap.ElementName, memberMap.GetSerializer()); | ||
} | ||
|
||
// otherwise fall back to calling TryGetMemberSerializationInfo on potential matches | ||
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.IgnoreCase; | ||
foreach (var memberInfo in documentSerializer.ValueType.GetMember(constructorParameterName, bindingFlags)) | ||
{ | ||
if (documentSerializer.TryGetMemberSerializationInfo(memberInfo.Name, out var serializationInfo)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While using a case insensitive match could give us some false hits, |
||
{ | ||
return (serializationInfo.ElementName, serializationInfo.Serializer); | ||
} | ||
} | ||
|
||
return (null, null); | ||
} | ||
|
||
private static BsonMemberMap FindMemberMap(Expression expression, BsonClassMap classMap, string memberName) | ||
{ | ||
foreach (var memberMap in classMap.DeclaredMemberMaps) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,15 +24,34 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggreg | |
{ | ||
internal static class NewArrayInitExpressionToAggregationExpressionTranslator | ||
{ | ||
public static AggregationExpression Translate(TranslationContext context, NewArrayExpression expression) | ||
public static AggregationExpression Translate(TranslationContext context, NewArrayExpression expression, IBsonSerializer resultSerializer) | ||
{ | ||
var items = new List<AstExpression>(); | ||
IBsonArraySerializer arraySerializer = null; | ||
IBsonSerializer itemSerializer = null; | ||
|
||
if (resultSerializer != null) | ||
{ | ||
if ((arraySerializer = resultSerializer as IBsonArraySerializer) == null) | ||
{ | ||
throw new ExpressionNotSupportedException(expression, because: $"serializer class {resultSerializer} does not implement IBsonArraySerializer"); | ||
} | ||
if (!arraySerializer.TryGetItemSerializationInfo(out var itemSerializationInfo)) | ||
{ | ||
throw new ExpressionNotSupportedException(expression, because: $"serializer class {resultSerializer} returned false for TryGetItemSerializationInfo"); | ||
} | ||
|
||
itemSerializer = itemSerializationInfo.Serializer; | ||
} | ||
|
||
var items = new List<AstExpression>(); | ||
foreach (var itemExpression in expression.Expressions) | ||
{ | ||
var itemTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, itemExpression); | ||
var itemTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, itemExpression, itemSerializer); | ||
items.Add(itemTranslation.Ast); | ||
itemSerializer ??= itemTranslation.Serializer; | ||
if (itemSerializer == null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the caller did not provide a |
||
{ | ||
itemSerializer = itemTranslation.Serializer; | ||
} | ||
|
||
// make sure all items are serialized using the same serializer | ||
if (!itemTranslation.Serializer.Equals(itemSerializer)) | ||
|
@@ -42,12 +61,14 @@ public static AggregationExpression Translate(TranslationContext context, NewArr | |
} | ||
|
||
var ast = AstExpression.ComputedArray(items); | ||
|
||
var arrayType = expression.Type; | ||
var itemType = arrayType.GetElementType(); | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. If the caller did not provide a Note: |
||
{ | ||
var arrayType = expression.Type; | ||
var itemType = arrayType.GetElementType(); | ||
itemSerializer ??= BsonSerializer.LookupSerializer(itemType); // if the array is empty itemSerializer will be null | ||
var arraySerializerType = typeof(ArraySerializer<>).MakeGenericType(itemType); | ||
arraySerializer = (IBsonArraySerializer)Activator.CreateInstance(arraySerializerType, itemSerializer); | ||
} | ||
|
||
return new AggregationExpression(expression, ast, arraySerializer); | ||
} | ||
|
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.