-
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-5552: Add support for $convert in LINQ #1659
base: main
Are you sure you want to change the base?
Conversation
return (T)constantExpression.Value; | ||
} | ||
|
||
private static BsonType GetBsonType(Type type) //TODO Do we have this kind of info somewhere else...? |
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.
Do we have this kind of mapping somewhere else in our code?
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 can't find any.
There probably is nowhere else in the code where we need to map a C# type to a BsonType
like this. Everywhere else it is up to the serializer to determine how the C# type is represented.
This is an unusual case because we decide to let the user express the C# return type using the TTo
generic type parameter, and we let the C# type imply the BsonType
.
toValue != null && | ||
onError == null && | ||
onNull == null) | ||
if (onError == null && onNull == null && subType == null && format == null && byteOrder == 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.
Consider formatting with one condition per line to make it easier to read:
if (to is AstConstantExpression toConstantExpression &&
(toConstantExpression.Value as BsonString)?.Value is { } toValue &&
subType == null &&
byteOrder == null &&
format == null &&
onError == null &&
onNull == 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.
Done
@@ -14,7 +14,6 @@ | |||
*/ | |||
|
|||
using System.Linq.Expressions; | |||
using MongoDB.Bson.IO; |
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.
Unnecessary diff.
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 noticed it while I was looking at the file. It's a very small change, do we really need to open another PR to fix this?
This is valid also for the other small changes unrelated to the PR.
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'm not suggesting we need another PR for this... I'm just saying we shouldn't modify files that have nothing at all to do with this PR.
This file can be "cleaned" up some future day when we are doing some work that actually involves this file.
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.
What is your current thinking on this unnecessary diff? Can it be removed from this PR?
...ssionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs
Outdated
Show resolved
Hide resolved
...ssionToAggregationExpressionTranslators/MemberExpressionToAggregationExpressionTranslator.cs
Outdated
Show resolved
Hide resolved
src/MongoDB.Driver/Mql.cs
Outdated
/// <summary> | ||
/// Represents the options parameter for <see cref="Mql.Convert{TFrom, TTo}(TFrom, ConvertOptions{TTo})"/>. | ||
/// </summary> | ||
public abstract class ConvertOptions |
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 should probably be in a its own file called ConvertOptions.cs
.
Along with ByteOrder
(unless you think ByteOrder
should be in its own file).
...ionExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslator.cs
Show resolved
Hide resolved
return new TranslatedExpression(expression, ast, serializer); | ||
} | ||
|
||
private static void ExtractOptionsFromConstantExpression(ConstantExpression constantExpression, out ByteOrder? byteOrder, out string format, out AstExpression onErrorAst, out AstExpression onNullAst, out BsonBinarySubType? subType) |
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.
See suggested TranslateOptions
above.
byteOrder = options.ByteOrder; | ||
} | ||
|
||
private static void ExtractOptionsFromMemberInitExpression(MemberInitExpression memberInitExpression, TranslationContext context, out ByteOrder? byteOrder, out string format, out AstExpression onErrorAst, out AstExpression onNullAst, out BsonBinarySubType? subType) |
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.
See suggested TranslateOptions above.
} | ||
} | ||
|
||
private static T GetConstantValue<T>(Expression expression, string fieldName) |
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.
Use existing expression.GetConstantValue<TValue>
instead.
return (T)constantExpression.Value; | ||
} | ||
|
||
private static BsonType GetBsonType(Type type) |
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.
Rename to TranslateToType
and return two things,, the BsonType
and the toSerializer
.
private static (BsonType ToBsonType, IBsonSerializer ToSerialzier) TranslateToType(Expression expression, Type toType)
{
return Type.GetTypeCode(Nullable.GetUnderlyingType(toType) ?? toType) switch
{
TypeCode.Boolean => (BsonType.Boolean, BooleanSerializer.Instance),
TypeCode.Byte => (BsonType.Int32, ByteSerializer.Instance),
TypeCode.Char => (BsonType.String, StringSerializer.Instance),
TypeCode.DateTime => (BsonType.DateTime, DateTimeSerializer.Instance),
TypeCode.Decimal => (BsonType.Decimal128, DecimalSerializer.Instance),
TypeCode.Double => (BsonType.Double, DoubleSerializer.Instance),
TypeCode.Int16 => (BsonType.Int32, Int16Serializer.Instance),
TypeCode.Int32 => (BsonType.Int32, Int32Serializer.Instance),
TypeCode.Int64 => (BsonType.Int64, Int64Serializer.Instance),
TypeCode.SByte => (BsonType.Int32, SByteSerializer.Instance),
TypeCode.Single => (BsonType.Double, DoubleSerializer.Instance),
TypeCode.String => (BsonType.String, StringSerializer.Instance),
TypeCode.UInt16 => (BsonType.Int32, UInt16Serializer.Instance),
TypeCode.UInt32 => (BsonType.Int64, Int32Serializer.Instance),
TypeCode.UInt64 => (BsonType.Decimal128, UInt64Serializer.Instance),
_ when toType == typeof(byte[]) => (BsonType.Binary, ByteArraySerializer.Instance),
_ when toType == typeof(BsonBinaryData) => (BsonType.Binary, BsonBinaryDataSerializer.Instance),
_ when toType == typeof(Decimal128) => (BsonType.Decimal128, Decimal128Serializer.Instance),
_ when toType == typeof(Guid) => (BsonType.Binary, GuidSerializer.StandardInstance),
_ when toType == typeof(ObjectId) => (BsonType.ObjectId, ObjectIdSerializer.Instance),
_ => throw new ExpressionNotSupportedException(expression, because: $"{toType} is not a valid TTo for Convert"
};
}
This will require adding a few missing XyzSerializer.Instance
properties. Follow the existing pattern from serializers that already have an Instance
property.
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.
Wouldn't it be convenient here to use the serializers registered in StandardSerializers
? It seems that that's the perfect class for what we're trying to do here, as it takes care also of nullable value types. Of course we'd need to specify the serializers that are not in that list.
@@ -14,7 +14,6 @@ | |||
*/ | |||
|
|||
using System.Linq.Expressions; | |||
using MongoDB.Bson.IO; |
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'm not suggesting we need another PR for this... I'm just saying we shouldn't modify files that have nothing at all to do with this PR.
This file can be "cleaned" up some future day when we are doing some work that actually involves this file.
...ionExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslator.cs
Show resolved
Hide resolved
src/MongoDB.Driver/ConvertOptions.cs
Outdated
private bool _onErrorWasSet; | ||
private TTo _onNull; | ||
private bool _onNullWasSet; | ||
private readonly IBsonSerializer _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.
Looks like you forgot to remove the serializer here.
...ionExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslator.cs
Outdated
Show resolved
Hide resolved
...ionExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslator.cs
Outdated
Show resolved
Hide resolved
...ionExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslator.cs
Outdated
Show resolved
Hide resolved
_ => throw new ExpressionNotSupportedException(expression, because: $"{toType} is not a valid TTo for Convert") | ||
}; | ||
|
||
return (bsonType, StandardSerializers.GetSerializer(toType)); |
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 don't like the idea of using the StandardSerializers
here.
We are working with a defined set of of toType
serializers that may or may not be the same as the StandardSerializers
. In fact they are not, as you discovered by having to handle some cases specially.
I liked your original switch where you handled all cases in one switch, just modified to return the serializer also:
private static (BsonType ToBsonType, IBsonSerializer ToSerialzier) TranslateToType(Expression expression, Type toType)
{
return Type.GetTypeCode(Nullable.GetUnderlyingType(toType) ?? toType) switch
{
TypeCode.Boolean => (BsonType.Boolean, BooleanSerializer.Instance),
TypeCode.Byte => (BsonType.Int32, ByteSerializer.Instance),
TypeCode.Char => (BsonType.String, StringSerializer.Instance),
TypeCode.DateTime => (BsonType.DateTime, DateTimeSerializer.Instance),
TypeCode.Decimal => (BsonType.Decimal128, DecimalSerializer.Instance),
TypeCode.Double => (BsonType.Double, DoubleSerializer.Instance),
TypeCode.Int16 => (BsonType.Int32, Int16Serializer.Instance),
TypeCode.Int32 => (BsonType.Int32, Int32Serializer.Instance),
TypeCode.Int64 => (BsonType.Int64, Int64Serializer.Instance),
TypeCode.SByte => (BsonType.Int32, SByteSerializer.Instance),
TypeCode.Single => (BsonType.Double, DoubleSerializer.Instance),
TypeCode.String => (BsonType.String, StringSerializer.Instance),
TypeCode.UInt16 => (BsonType.Int32, UInt16Serializer.Instance),
TypeCode.UInt32 => (BsonType.Int64, Int32Serializer.Instance),
TypeCode.UInt64 => (BsonType.Decimal128, UInt64Serializer.Instance),
_ when toType == typeof(byte[]) => (BsonType.Binary, ByteArraySerializer.Instance),
_ when toType == typeof(BsonBinaryData) => (BsonType.Binary, BsonBinaryDataSerializer.Instance),
_ when toType == typeof(Decimal128) => (BsonType.Decimal128, Decimal128Serializer.Instance),
_ when toType == typeof(Guid) => (BsonType.Binary, GuidSerializer.StandardInstance),
_ when toType == typeof(ObjectId) => (BsonType.ObjectId, ObjectIdSerializer.Instance),
_ => throw new ExpressionNotSupportedException(expression, because: $"{toType} is not a valid TTo for Convert")
};
}
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'll go back to this, but we still need to take care of nullable types that were already registered in StandardSerializers
. This also means that we'll need to create the nullable serializers here from scratch, as we don't have static instances we can use.
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 forgot that TTo
could be nullable. Does that change what you want to do?
My initial concern with using StandardSerializers
was that I wasn't 100% sure what "Standard" meant and whether it would always result in the right thing being done (for Convert).
I was also concerned that some of the serializers needed by Convert might not belong in the "Standard" set.
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.
It's true that some of the serializers need by convert are not in the "Standard" set, but it seems that it covered most of the "basic" types. It seems that StandardSerializers
' methods are used by ConstantExpressionToAggregationExpressionTranslator
to serialize the constant value, so I thought this could be a similar thing.
Anyways, I think the current solution makes sense, the only small annoyance is that we don't have static "Instances" of the nullable serializers, but I think it's a small thing.
{ | ||
var underlyingType = Nullable.GetUnderlyingType(toType); | ||
var isNullable = underlyingType != null; | ||
var effectiveType = underlyingType ?? toType; |
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 very much dislike calling Nullable.GetUnderlyingType
when we do NOT know if the type is nullable or not. It seems like this method should throw an exception when called with a non-nullable type. I know it does not, but that seems more like a quirk and an implementation error on Microsoft's part, one that we should not rely on.
I suggest:
var isNullable = toType.IsNullable();
var valueType = isNullable ? Nullable.GetUnderlyingType(toType) : toType;
effectiveType
is not a terrible name, but we tend to use valueType
when talking about nullable values.
var isNullable = underlyingType != null; | ||
var effectiveType = underlyingType ?? toType; | ||
|
||
(BsonType, IBsonSerializer) result = Type.GetTypeCode(effectiveType) switch |
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 was trying to figure out why you had to use this odd result
variable here...
The reason is that the switch
expression doesn't have a "best" result type that applies to all cases, and the compiler needs some guidance. The declaration of the result
variable is one way to provide that guidance.
Consider this alternative that doesn't require the intermediate result
variable:
var (bsonType, valueSerializer) = (ValueTuple<BsonType, IBsonSerializer>)(Type.GetTypeCode(valueType) switch
{
....
});
where casting the result of the switch
expression to the ValueTuple
provides the guidance to the compiler.
Alternatively, if you were to cast even ONE of the results to IBsonSerializer
that would work also, but it is a bit esoteric to do it that way:
TypeCode.Boolean => (BsonType.Boolean, (IBsonSerializer)BooleanSerializer.Instance),
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.
That's cool, I didn't think about it.
@@ -14,7 +14,6 @@ | |||
*/ | |||
|
|||
using System.Linq.Expressions; | |||
using MongoDB.Bson.IO; |
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.
What is your current thinking on this unnecessary diff? Can it be removed from this PR?
src/MongoDB.Driver/ConvertOptions.cs
Outdated
set => _subType = value; | ||
} | ||
|
||
internal abstract bool OnErrorWasSet(out object onError); |
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.
Should it be named TryGetOnError
?
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 it would make sense.
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.
Please check evergreen also. There are some test failures related to the changes.
|
||
internal abstract bool TryGetOnError(out object onError); | ||
|
||
internal abstract bool TryGetOnNull(out object onNull); |
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'm not convinced this name change is a good one.
Not every method that returns a bool
should be named TryXyz
.
In my opinion methods should only be named TryXyz
when they have a chance of failure, but we want to return false
instead of throwing an exception.
In this case there is no chance of failure. We merely want to know whether OnError
or OnNull
was set, and if so to what.
No description provided.