diff --git a/src/MongoDB.Bson/Serialization/Serializers/ByteSerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/ByteSerializer.cs index 1f27a5aeeb1..f8043edbe3a 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/ByteSerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/ByteSerializer.cs @@ -23,6 +23,16 @@ namespace MongoDB.Bson.Serialization.Serializers /// public sealed class ByteSerializer : StructSerializerBase, IRepresentationConfigurable { + #region static + private static readonly ByteSerializer __instance = new(); + + // public static properties + /// + /// Gets a cached instance of a default ByteSerializer. + /// + public static ByteSerializer Instance => __instance; + #endregion + // private fields private readonly BsonType _representation; diff --git a/src/MongoDB.Bson/Serialization/Serializers/Int16Serializer.cs b/src/MongoDB.Bson/Serialization/Serializers/Int16Serializer.cs index 0b9c2a1ce50..90679385bde 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/Int16Serializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/Int16Serializer.cs @@ -24,6 +24,15 @@ namespace MongoDB.Bson.Serialization.Serializers /// public sealed class Int16Serializer : StructSerializerBase, IRepresentationConfigurable, IRepresentationConverterConfigurable { + #region static + private static readonly Int16Serializer __instance = new(); + + /// + /// Gets a cached instance of an Int16Serializer; + /// + public static Int16Serializer Instance => __instance; + #endregion + // private fields private readonly BsonType _representation; private readonly RepresentationConverter _converter; diff --git a/src/MongoDB.Bson/Serialization/Serializers/SByteSerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/SByteSerializer.cs index 4245ff29dfa..9065a778e96 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/SByteSerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/SByteSerializer.cs @@ -24,6 +24,15 @@ namespace MongoDB.Bson.Serialization.Serializers [CLSCompliant(false)] public sealed class SByteSerializer : StructSerializerBase, IRepresentationConfigurable { + #region static + private static readonly SByteSerializer __instance = new(); + + /// + /// Gets a cached instance of an SByteSerializer; + /// + public static SByteSerializer Instance => __instance; + #endregion + // private fields private readonly BsonType _representation; diff --git a/src/MongoDB.Bson/Serialization/Serializers/SingleSerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/SingleSerializer.cs index 24301520235..9d3e691ebac 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/SingleSerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/SingleSerializer.cs @@ -24,6 +24,15 @@ namespace MongoDB.Bson.Serialization.Serializers /// public sealed class SingleSerializer : StructSerializerBase, IRepresentationConfigurable, IRepresentationConverterConfigurable { + #region static + private static readonly SingleSerializer __instance = new(); + + /// + /// Gets a cached instance of an SingleSerializer; + /// + public static SingleSerializer Instance => __instance; + #endregion + // private fields private readonly BsonType _representation; private readonly RepresentationConverter _converter; diff --git a/src/MongoDB.Bson/Serialization/Serializers/UInt16Serializer.cs b/src/MongoDB.Bson/Serialization/Serializers/UInt16Serializer.cs index dd75fb4190f..6e1bc6dca35 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/UInt16Serializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/UInt16Serializer.cs @@ -25,6 +25,15 @@ namespace MongoDB.Bson.Serialization.Serializers [CLSCompliant(false)] public sealed class UInt16Serializer : StructSerializerBase, IRepresentationConfigurable, IRepresentationConverterConfigurable { + #region static + private static readonly UInt16Serializer __instance = new(); + + /// + /// Gets a cached instance of an UInt16Serializer; + /// + public static UInt16Serializer Instance => __instance; + #endregion + // private fields private readonly BsonType _representation; private readonly RepresentationConverter _converter; diff --git a/src/MongoDB.Bson/Serialization/Serializers/UInt64Serializer.cs b/src/MongoDB.Bson/Serialization/Serializers/UInt64Serializer.cs index 88bfe7a101d..693b8f6cbf3 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/UInt64Serializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/UInt64Serializer.cs @@ -25,6 +25,15 @@ namespace MongoDB.Bson.Serialization.Serializers [CLSCompliant(false)] public sealed class UInt64Serializer : StructSerializerBase, IRepresentationConfigurable, IRepresentationConverterConfigurable { + #region static + private static readonly UInt64Serializer __instance = new(); + + /// + /// Gets a cached instance of an UInt64Serializer; + /// + public static UInt64Serializer Instance => __instance; + #endregion + // private fields private readonly BsonType _representation; private readonly RepresentationConverter _converter; diff --git a/src/MongoDB.Driver/ConvertOptions.cs b/src/MongoDB.Driver/ConvertOptions.cs new file mode 100644 index 00000000000..bb870406106 --- /dev/null +++ b/src/MongoDB.Driver/ConvertOptions.cs @@ -0,0 +1,126 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using MongoDB.Bson; + +namespace MongoDB.Driver +{ + /// + /// Represents the options parameter for . + /// + public abstract class ConvertOptions + { + private ByteOrder? _byteOrder; + private string _format; + private BsonBinarySubType? _subType; + + /// + /// The byteOrder parameter. + /// + public ByteOrder? ByteOrder + { + get => _byteOrder; + set => _byteOrder = value; + } + + /// + /// The format parameter. + /// + public string Format + { + get => _format; + set => _format = value; + } + + /// + /// The subType parameter. + /// + public BsonBinarySubType? SubType + { + get => _subType; + set => _subType = value; + } + + internal abstract bool OnErrorWasSet(out object onError); + + internal abstract bool OnNullWasSet(out object onNull); + } + + /// + /// Represents the options parameter for . + /// This class allows to set 'onError' and 'onNull'. + /// + /// The type of 'onError' and 'onNull'. + public class ConvertOptions : ConvertOptions + { + private TTo _onError; + private bool _onErrorWasSet; + private TTo _onNull; + private bool _onNullWasSet; + + /// + /// The onError parameter. + /// + public TTo OnError + { + get => _onError; + set + { + _onError = value; + _onErrorWasSet = true; + } + } + + /// + /// The onNull parameter. + /// + public TTo OnNull + { + get => _onNull; + set + { + _onNull = value; + _onNullWasSet = true; + } + } + + internal override bool OnErrorWasSet(out object onError) + { + onError = _onError; + return _onErrorWasSet; + } + + internal override bool OnNullWasSet(out object onNull) + { + onNull = _onNull; + return _onNullWasSet; + } + } + + /// + /// Represents the byte order of binary data when converting to/from numerical types using . + /// + public enum ByteOrder + { + /// + /// Big endian order. + /// + BigEndian, + /// + /// Little endian order. + /// + LittleEndian, + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Core/Misc/Feature.cs b/src/MongoDB.Driver/Core/Misc/Feature.cs index 720cf3c19db..0a59eb0bf66 100644 --- a/src/MongoDB.Driver/Core/Misc/Feature.cs +++ b/src/MongoDB.Driver/Core/Misc/Feature.cs @@ -44,6 +44,8 @@ public class Feature private static readonly Feature __clientBulkWrite = new Feature("ClientBulkWrite", WireVersion.Server80); private static readonly Feature __clientSideEncryption = new Feature("ClientSideEncryption", WireVersion.Server42); private static readonly Feature __clusteredIndexes = new Feature("ClusteredIndexes", WireVersion.Server53); + private static readonly Feature __convertOperatorBinDataToFromNumeric = new Feature("ConvertOperatorBinDataToFromNumeric", WireVersion.Server81); + private static readonly Feature __convertOperatorBinDataToFromString= new Feature("ConvertOperatorBinDataToFromString", WireVersion.Server80); private static readonly Feature __createIndexCommitQuorum = new Feature("CreateIndexCommitQuorum", WireVersion.Server44); private static readonly Feature __createIndexesUsingInsertOperations = new Feature("CreateIndexesUsingInsertOperations", WireVersion.Zero, WireVersion.Server42); private static readonly Feature __csfleRangeAlgorithm = new Feature("CsfleRangeAlgorithm", WireVersion.Server62); @@ -193,6 +195,16 @@ public class Feature /// public static Feature ClusteredIndexes => __clusteredIndexes; + /// + /// Gets the conversion of binary data to/from numeric types feature. + /// + public static Feature ConvertOperatorBinDataToFromNumeric => __convertOperatorBinDataToFromNumeric; + + /// + /// Gets the conversion of binary data to/from string feature. + /// + public static Feature ConvertOperatorBinDataToFromString => __convertOperatorBinDataToFromString; + /// /// Gets the create index commit quorum feature. /// diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstEnumExtensions.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstEnumExtensions.cs new file mode 100644 index 00000000000..740cc624f94 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/AstEnumExtensions.cs @@ -0,0 +1,61 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using MongoDB.Bson; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Ast +{ + internal static class AstEnumExtensions + { + public static string Render(this BsonType type) + { + return type switch + { + BsonType.Array => "array", + BsonType.Binary => "binData", + BsonType.Boolean => "bool", + BsonType.DateTime => "date", + BsonType.Decimal128 => "decimal", + BsonType.Document => "object", + BsonType.Double => "double", + BsonType.Int32 => "int", + BsonType.Int64 => "long", + BsonType.JavaScript => "javascript", + BsonType.JavaScriptWithScope => "javascriptWithScope", + BsonType.MaxKey => "maxKey", + BsonType.MinKey => "minKey", + BsonType.Null => "null", + BsonType.ObjectId => "objectId", + BsonType.RegularExpression => "regex", + BsonType.String => "string", + BsonType.Symbol => "symbol", + BsonType.Timestamp => "timestamp", + BsonType.Undefined => "undefined", + _ => throw new ArgumentException($"Unexpected BSON type: {type}.", nameof(type)) + }; + } + + public static string Render(this ByteOrder byteOrder) + { + return byteOrder switch + { + ByteOrder.BigEndian => "big", + ByteOrder.LittleEndian => "little", + _ => throw new ArgumentException($"Unexpected {nameof(ByteOrder)}: {byteOrder}.", nameof(byteOrder)) + }; + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstConvertExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstConvertExpression.cs index 5f78f4a89bd..b1b1497bab2 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstConvertExpression.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstConvertExpression.cs @@ -21,27 +21,39 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions { internal sealed class AstConvertExpression : AstExpression { + private readonly ByteOrder? _byteOrder; + private readonly string _format; private readonly AstExpression _input; private readonly AstExpression _onError; private readonly AstExpression _onNull; + private readonly BsonBinarySubType? _subType; private readonly AstExpression _to; public AstConvertExpression( AstExpression input, AstExpression to, + BsonBinarySubType? subType = null, + ByteOrder? byteOrder = null, + string format = null, AstExpression onError = null, AstExpression onNull = null) { _input = Ensure.IsNotNull(input, nameof(input)); _to = Ensure.IsNotNull(to, nameof(to)); + _subType = subType; + _byteOrder = byteOrder; + _format = format; _onError = onError; _onNull = onNull; } + public ByteOrder? ByteOrder => _byteOrder; + public string Format => _format; public AstExpression Input => _input; public override AstNodeType NodeType => AstNodeType.ConvertExpression; public AstExpression OnError => _onError; public AstExpression OnNull => _onNull; + public BsonBinarySubType? SubType => _subType; public AstExpression To => _to; public override AstNode Accept(AstNodeVisitor visitor) @@ -56,9 +68,18 @@ public override BsonValue Render() { "$convert", new BsonDocument { { "input", _input.Render() }, - { "to", _to.Render() }, + { "to", _to.Render(), _subType == null }, + { "to", () => new BsonDocument + { + { "type", _to.Render() }, + { "subtype", (int)_subType!.Value}, + }, + _subType != null + }, { "onError", () => _onError.Render(), _onError != null }, - { "onNull", () => _onNull.Render(), _onNull != null } + { "onNull", () => _onNull.Render(), _onNull != null }, + { "format", () => _format, _format != null }, + { "byteOrder", () => _byteOrder!.Value.Render(), _byteOrder != null } } } }; @@ -75,7 +96,7 @@ public AstConvertExpression Update( return this; } - return new AstConvertExpression(input, to, onError, onNull); + return new AstConvertExpression(input, to, _subType, _byteOrder, _format, onError, onNull); } } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs index eb37ffde294..fa42427419a 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs @@ -255,14 +255,23 @@ public static AstExpression Constant(BsonValue value) return new AstConstantExpression(value); } - public static AstExpression Convert(AstExpression input, AstExpression to, AstExpression onError = null, AstExpression onNull = null) + public static AstExpression Convert( + AstExpression input, + AstExpression to, + BsonBinarySubType? subType = null, + ByteOrder? byteOrder = null, + string format = null, + AstExpression onError = null, + AstExpression onNull = null) { Ensure.IsNotNull(input, nameof(input)); Ensure.IsNotNull(to, nameof(to)); if (to is AstConstantExpression toConstantExpression && - (toConstantExpression.Value as BsonString)?.Value is string toValue && - toValue != null && + (toConstantExpression.Value as BsonString)?.Value is { } toValue && + subType == null && + byteOrder == null && + format == null && onError == null && onNull == null) { @@ -278,13 +287,14 @@ public static AstExpression Convert(AstExpression input, AstExpression to, AstEx "string" => AstUnaryOperator.ToString, _ => (AstUnaryOperator?)null }; + if (unaryOperator.HasValue) { return AstExpression.Unary(unaryOperator.Value, input); } } - return new AstConvertExpression(input, to, onError, onNull); + return new AstConvertExpression(input, to, subType, byteOrder, format, onError, onNull); } public static AstExpression DateAdd( diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/SerializationHelper.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/SerializationHelper.cs index a0377fdea76..0b34b0bd7cf 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/SerializationHelper.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/SerializationHelper.cs @@ -77,6 +77,14 @@ public static void EnsureRepresentationIsNumeric(Expression expression, Expressi } } + public static void EnsureSerializerIsCompatible(Expression expression, Expression containingExpression, IBsonSerializer actualSerializer, IBsonSerializer expectedSerializer) + { + if (!actualSerializer.Equals(expectedSerializer)) + { + throw new ExpressionNotSupportedException(expression, containingExpression, because: "the result serializer is not compatible with the expected serializer"); + } + } + public static BsonType GetRepresentation(IBsonSerializer serializer) { if (serializer is IDiscriminatedInterfaceSerializer discriminatedInterfaceSerializer) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs index e04f8d50d13..02916395b57 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs @@ -25,6 +25,7 @@ internal static class MqlMethod // private static fields private static readonly MethodInfo __constantWithRepresentation; private static readonly MethodInfo __constantWithSerializer; + private static readonly MethodInfo __convert; private static readonly MethodInfo __dateFromString; private static readonly MethodInfo __dateFromStringWithFormat; private static readonly MethodInfo __dateFromStringWithFormatAndTimezone; @@ -39,6 +40,7 @@ static MqlMethod() { __constantWithRepresentation = ReflectionInfo.Method((object value, BsonType representation) => Mql.Constant(value, representation)); __constantWithSerializer = ReflectionInfo.Method((object value, IBsonSerializer serializer) => Mql.Constant(value, serializer)); + __convert = ReflectionInfo.Method((object value, ConvertOptions options) => Mql.Convert(value, options)); __dateFromString = ReflectionInfo.Method((string dateStringl) => Mql.DateFromString(dateStringl)); __dateFromStringWithFormat = ReflectionInfo.Method((string dateString, string format) => Mql.DateFromString(dateString, format)); __dateFromStringWithFormatAndTimezone = ReflectionInfo.Method((string dateString, string format, string timezone) => Mql.DateFromString(dateString, format, timezone)); @@ -52,6 +54,7 @@ static MqlMethod() // public properties public static MethodInfo ConstantWithRepresentation => __constantWithRepresentation; public static MethodInfo ConstantWithSerializer => __constantWithSerializer; + public static MethodInfo Convert => __convert; public static MethodInfo DateFromString => __dateFromString; public static MethodInfo DateFromStringWithFormat => __dateFromStringWithFormat; public static MethodInfo DateFromStringWithFormatAndTimezone => __dateFromStringWithFormatAndTimezone; diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs index 5eb324df3fd..3054156427a 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs @@ -39,6 +39,7 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC case "Contains": return ContainsMethodToAggregationExpressionTranslator.Translate(context, expression); case "ContainsKey": return ContainsKeyMethodToAggregationExpressionTranslator.Translate(context, expression); case "ContainsValue": return ContainsValueMethodToAggregationExpressionTranslator.Translate(context, expression); + case "Convert": return ConvertMethodToAggregationExpressionTranslator.Translate(context, expression); case "CovariancePopulation": return CovariancePopulationMethodToAggregationExpressionTranslator.Translate(context, expression); case "CovarianceSample": return CovarianceSampleMethodToAggregationExpressionTranslator.Translate(context, expression); case "Create": return CreateMethodToAggregationExpressionTranslator.Translate(context, expression); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslator.cs new file mode 100644 index 00000000000..9f6844b3031 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslator.cs @@ -0,0 +1,181 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Linq.Expressions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver.Linq.Linq3Implementation.Ast; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Reflection; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators +{ + internal class ConvertMethodToAggregationExpressionTranslator + { + public static TranslatedExpression Translate(TranslationContext context, MethodCallExpression expression) + { + var method = expression.Method; + var arguments = expression.Arguments; + + if (!method.Is(MqlMethod.Convert)) + { + throw new ExpressionNotSupportedException(expression); + } + + var toType = method.GetGenericArguments()[1]; + var valueExpression = arguments[0]; + var optionsExpression = arguments[1]; + + var (toBsonType, toSerializer) = TranslateToType(expression, toType); + var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression); + var (subType, byteOrder, format, onErrorAst, onNullAst) = TranslateOptions(context, expression, optionsExpression, toSerializer); + + var ast = AstExpression.Convert(valueTranslation.Ast, toBsonType.Render(), subType, byteOrder, format, onErrorAst, onNullAst); + return new TranslatedExpression(expression, ast, toSerializer); + } + + private static (BsonBinarySubType? subType, ByteOrder? byteOrder, string format, AstExpression onErrorAst, AstExpression onNullAst) + TranslateOptions( + TranslationContext context, + Expression expression, + Expression optionsExpression, + IBsonSerializer toSerializer) + { + return optionsExpression switch + { + ConstantExpression constantExpression => TranslateOptions(constantExpression, toSerializer), + MemberInitExpression memberInitExpressionExpression => TranslateOptions(context, expression, memberInitExpressionExpression, toSerializer), + _ => throw new ExpressionNotSupportedException(optionsExpression, containingExpression: expression, because: "the options argument must be either a constant or a member initialization expression.") + }; + } + + private static (BsonBinarySubType? subType, ByteOrder? byteOrder, string format, AstExpression onErrorAst, AstExpression onNullAst) + TranslateOptions( + ConstantExpression optionsExpression, + IBsonSerializer toSerializer) + { + var options = (ConvertOptions)optionsExpression.Value; + + AstExpression onErrorAst = null; + AstExpression onNullAst = null; + if (options != null) + { + if (options.OnErrorWasSet(out var onErrorValue)) + { + var serializedOnErrorValue = SerializationHelper.SerializeValue(toSerializer, onErrorValue); + onErrorAst = AstExpression.Constant(serializedOnErrorValue); + } + + if (options.OnNullWasSet(out var onNullValue)) + { + var serializedOnNullValue = SerializationHelper.SerializeValue(toSerializer, onNullValue); + onNullAst = AstExpression.Constant(serializedOnNullValue); + } + } + + return (options?.SubType, options?.ByteOrder, options?.Format, onErrorAst, onNullAst); + } + + private static (BsonBinarySubType? subType, ByteOrder? byteOrder, string format, AstExpression onErrorAst, AstExpression onNullAst) + TranslateOptions( + TranslationContext context, + Expression expression, + MemberInitExpression optionsExpression, + IBsonSerializer toSerializer + ) + { + BsonBinarySubType? subType = null; + ByteOrder? byteOrder = null; + string format = null; + TranslatedExpression onErrorTranslation = null; + TranslatedExpression onNullTranslation = null; + + foreach (var binding in optionsExpression.Bindings) + { + if (binding is not MemberAssignment memberAssignment) + { + throw new ExpressionNotSupportedException(optionsExpression, containingExpression: expression, because: "only member assignment is supported"); + } + + var memberName = memberAssignment.Member.Name; + var memberExpression = memberAssignment.Expression; + + switch (memberName) + { + case nameof(ConvertOptions.ByteOrder): + byteOrder = memberExpression.GetConstantValue(expression); + break; + case nameof(ConvertOptions.Format): + format = memberExpression.GetConstantValue(expression); + break; + case nameof(ConvertOptions.OnError): + onErrorTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, memberExpression); + SerializationHelper.EnsureSerializerIsCompatible(memberExpression, containingExpression: expression, onErrorTranslation.Serializer, expectedSerializer: toSerializer); + break; + case nameof(ConvertOptions.OnNull): + onNullTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, memberExpression); + SerializationHelper.EnsureSerializerIsCompatible(memberExpression, containingExpression: expression, onNullTranslation.Serializer, expectedSerializer: toSerializer); + break; + case nameof(ConvertOptions.SubType): + subType = memberExpression.GetConstantValue(expression); + break; + default: + throw new ExpressionNotSupportedException(memberExpression, because: $"memberName {memberName} is invalid"); + } + } + + return (subType, byteOrder, format, onErrorTranslation?.Ast, onNullTranslation?.Ast); + } + + private static (BsonType ToBsonType, IBsonSerializer ToSerializer) TranslateToType(Expression expression, Type toType) + { + var isNullable = toType.IsNullable(); + var valueType = isNullable ? Nullable.GetUnderlyingType(toType) : toType; + + var (bsonType, valueSerializer) = (ValueTuple)(Type.GetTypeCode(valueType) 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, SingleSerializer.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 valueType == typeof(byte[]) => (BsonType.Binary, ByteArraySerializer.Instance), + _ when valueType == typeof(BsonBinaryData) => (BsonType.Binary, BsonBinaryDataSerializer.Instance), + _ when valueType == typeof(Decimal128) => (BsonType.Decimal128, Decimal128Serializer.Instance), + _ when valueType == typeof(Guid) => (BsonType.Binary, GuidSerializer.StandardInstance), + _ when valueType == typeof(ObjectId) => (BsonType.ObjectId, ObjectIdSerializer.Instance), + + _ => throw new ExpressionNotSupportedException(expression, because: $"{toType} is not a valid TTo for Convert") + }); + + return (bsonType, isNullable ? NullableSerializer.Create(valueSerializer) : valueSerializer); + } + } +} diff --git a/src/MongoDB.Driver/Mql.cs b/src/MongoDB.Driver/Mql.cs index e1ccf7ef4ce..d0e0df4c106 100644 --- a/src/MongoDB.Driver/Mql.cs +++ b/src/MongoDB.Driver/Mql.cs @@ -17,6 +17,7 @@ using MongoDB.Driver.Linq.Linq3Implementation.Misc; using MongoDB.Bson; using MongoDB.Bson.Serialization; +using MongoDB.Driver.Linq.Linq3Implementation.Serializers; namespace MongoDB.Driver { @@ -49,6 +50,20 @@ public static TValue Constant(TValue value, BsonType representaion) throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); } + /// + /// Converts a value from one type to another using the $convert aggregation operator. + /// + /// The type of the input value. + /// The type of the output value. + /// The value to convert. + /// The conversion options. + /// The converted value. + /// Not all conversions are supported by the $convert operator. + public static TTo Convert(TFrom value, ConvertOptions options) + { + throw CustomLinqExtensionMethodHelper.CreateNotSupportedException(); + } + /// /// Converts a string to a DateTime using the $dateFromString aggregation operator. /// diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Ast/Expressions/AstExpressionTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Ast/Expressions/AstExpressionTests.cs index cfb89cb48b0..0f313e27f50 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Ast/Expressions/AstExpressionTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Ast/Expressions/AstExpressionTests.cs @@ -97,7 +97,7 @@ public void Convert_with_on_error_should_return_long_form(string toValue) var to = AstExpression.Constant(toValue); var onError = AstExpression.Constant(BsonNull.Value); - var result = AstExpression.Convert(input, to, onError, onNull: null); + var result = AstExpression.Convert(input, to, onError: onError, onNull: null); var convertExpression = result.Should().BeOfType().Subject; convertExpression.Input.Should().BeSameAs(input); @@ -121,7 +121,7 @@ public void Convert_with_on_null_should_return_long_form(string toValue) var to = AstExpression.Constant(toValue); var onNull = AstExpression.Constant(BsonNull.Value); - var result = AstExpression.Convert(input, to, onError: null, onNull); + var result = AstExpression.Convert(input, to, onError: null, onNull: onNull); var convertExpression = result.Should().BeOfType().Subject; convertExpression.Input.Should().BeSameAs(input); diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslatorTests.cs new file mode 100644 index 00000000000..7759baf34dd --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ConvertMethodToAggregationExpressionTranslatorTests.cs @@ -0,0 +1,585 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Driver.Core.Misc; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; +using MongoDB.Driver.Linq; +using MongoDB.Driver.TestHelpers; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators +{ + public class ConvertMethodToAggregationExpressionTranslatorTests : + LinqIntegrationTest + { + public ConvertMethodToAggregationExpressionTranslatorTests(ClassFixture fixture) + : base(fixture) + { + } + + [Theory] + [InlineData(4, ByteOrder.LittleEndian,"ogIAAA==", null)] + [InlineData(6, ByteOrder.BigEndian, "AAAAKg==", null )] + [InlineData(10, ByteOrder.BigEndian, null, "MongoCommandException")] + public void Convert_to_BsonBinaryData_from_int_should_work(int id, ByteOrder byteOrder, string expectedBase64, string expectedException) + { + RequireServer.Check().Supports(Feature.ConvertOperatorBinDataToFromNumeric); + + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.IntProperty, new ConvertOptions { ByteOrder = byteOrder, SubType = BsonBinarySubType.Binary })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$IntProperty', to : {{ type: 'binData', subtype: 0 }}, {ByteOrderToString(byteOrder)} }} }}, _id : 0 }} }}", + }; + + var expectedResult = expectedBase64 == null ? default : new BsonBinaryData(Convert.FromBase64String(expectedBase64)); + AssertOutcome(collection, queryable, expectedStages, expectedResult, expectedException); + } + + [Theory] + [InlineData(4, ByteOrder.LittleEndian,"ogIAAA==", null)] + [InlineData(6, ByteOrder.BigEndian, "AAAAKg==", null )] + [InlineData(10, ByteOrder.BigEndian, null, "MongoCommandException")] + public void Convert_to_BsonBinaryData_from_long_should_work(int id, ByteOrder byteOrder, string expectedBase64, string expectedException) + { + RequireServer.Check().Supports(Feature.ConvertOperatorBinDataToFromNumeric); + + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.LongProperty, new ConvertOptions { ByteOrder = byteOrder, SubType = BsonBinarySubType.Binary })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$LongProperty', to : {{ type: 'binData', subtype: 0 }}, {ByteOrderToString(byteOrder)} }} }}, _id : 0 }} }}", + }; + + BsonBinaryData expectedResult = null; + + if (expectedBase64 is not null) + { + //$convert to BinData returns always 8 bytes when from long + var expectedBytes = new byte[8]; + Array.Copy(Convert.FromBase64String(expectedBase64), 0, expectedBytes, byteOrder is ByteOrder.LittleEndian ? 0 : 4, 4); + expectedResult = new BsonBinaryData(expectedBytes); + } + + AssertOutcome(collection, queryable, expectedStages, expectedResult, expectedException); + } + + [Theory] + [InlineData(3, ByteOrder.LittleEndian,"AAAAAAAA4L8=", null)] + [InlineData(5, ByteOrder.BigEndian, "wAQAAAAAAAA=", null )] + [InlineData(10, ByteOrder.BigEndian, null, "MongoCommandException")] + public void Convert_to_BsonBinaryData_from_double_should_work(int id, ByteOrder byteOrder, string expectedBase64, string expectedException) + { + RequireServer.Check().Supports(Feature.ConvertOperatorBinDataToFromNumeric); + + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.DoubleProperty, new ConvertOptions { ByteOrder = byteOrder, SubType = BsonBinarySubType.Binary })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$DoubleProperty', to : {{ type: 'binData', subtype: 0 }}, {ByteOrderToString(byteOrder)} }} }}, _id : 0 }} }}", + }; + + var expectedResult = expectedBase64 == null ? default : new BsonBinaryData(Convert.FromBase64String(expectedBase64)); + AssertOutcome(collection, queryable, expectedStages, expectedResult, expectedException); + } + + [Theory] + [InlineData(2, ByteOrder.LittleEndian, 0, "MongoCommandException")] + [InlineData(4, ByteOrder.LittleEndian, 674, null)] + [InlineData(6, ByteOrder.BigEndian, 42, null)] + public void Convert_to_int_from_BsonBinaryData_should_work(int id, ByteOrder byteOrder, int expectedResult, string expectedException) + { + RequireServer.Check().Supports(Feature.ConvertOperatorBinDataToFromNumeric); + + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.BinaryProperty, new ConvertOptions { ByteOrder = byteOrder })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$BinaryProperty', to : 'int', {ByteOrderToString(byteOrder)} }} }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, expectedResult, expectedException); + } + + [Theory] + [InlineData(2, ByteOrder.LittleEndian, 0, "MongoCommandException")] + [InlineData(4, ByteOrder.LittleEndian, (long)674, null)] + [InlineData(6, ByteOrder.BigEndian, (long)42, null)] + public void Convert_to_long_from_BsonBinaryData_should_work(int id, ByteOrder byteOrder, long expectedResult, string expectedException) + { + RequireServer.Check().Supports(Feature.ConvertOperatorBinDataToFromNumeric); + + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.BinaryProperty, new ConvertOptions { ByteOrder = byteOrder })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$BinaryProperty', to : 'long', {ByteOrderToString(byteOrder)} }} }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, expectedResult, expectedException); + } + + [Theory] + [InlineData(2, ByteOrder.BigEndian, 0, "MongoCommandException")] + [InlineData(3, ByteOrder.LittleEndian, -0.5, null)] + [InlineData(5, ByteOrder.BigEndian, -2.5, null)] + public void Convert_to_double_from_BsonBinaryData_should_work(int id, ByteOrder byteOrder, double expectedResult, string expectedException) + { + RequireServer.Check().Supports(Feature.ConvertOperatorBinDataToFromNumeric); + + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.BinaryProperty, new ConvertOptions { ByteOrder = byteOrder })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$BinaryProperty', to : 'double', {ByteOrderToString(byteOrder)} }} }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, expectedResult, expectedException); + } + + [Theory] + [InlineData(null)] + [InlineData(25)] + public void Convert_with_constant_OnError_should_work(int? onError) + { + const int id = 20; + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.StringProperty, new ConvertOptions { OnError = onError })); + + var onErrorVal = onError == null ? "null" : onError.ToString(); + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$StringProperty', to : 'int', onError: {onErrorVal} }} }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, onError); + } + + [Theory] + [InlineData(null)] + [InlineData(25)] + public void Convert_with_constant_OnNull_should_work(int? onNull) + { + const int id = 0; + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.StringProperty, new ConvertOptions { OnNull = onNull })); + + var onNullVal = onNull == null ? "null" : onNull.ToString(); + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$StringProperty', to : 'int', onNull: {onNullVal} }} }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, onNull); + } + + [Fact] + public void Convert_with_field_OnError_should_work() + { + const int id = 20; + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.StringProperty, new ConvertOptions { OnError = x.IntProperty })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$StringProperty', to : 'int', onError: '$IntProperty' }} }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, 22); + } + + [Fact] + public void Convert_with_field_OnNull_should_work() + { + const int id = 22; + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.StringProperty, new ConvertOptions { OnNull = x.IntProperty })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$StringProperty', to : 'int', onNull: '$IntProperty' }} }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, 33); + } + + [Theory] + [InlineData("uuid", "867dee52-c331-484e-92d1-c56479b8e67e")] + [InlineData("base64", "hn3uUsMxSE6S0cVkebjmfg==")] + public void Convert_with_format_should_be_rendered_correctly(string format, string expectedResult) + { + RequireServer.Check().Supports(Feature.ConvertOperatorBinDataToFromString); + + const int id = 2; + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.BinaryProperty, new ConvertOptions { Format = format })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$BinaryProperty', to : 'string', format: '{format}' }} }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, expectedResult); + } + + [Theory] + [InlineData(3, ByteOrder.LittleEndian,"AAAAAAAA4L8=")] + [InlineData(5, ByteOrder.BigEndian, "wAQAAAAAAAA=" )] + public void Convert_with_byteOrder_should_be_rendered_correctly(int id, ByteOrder byteOrder, string expectedBase64) + { + RequireServer.Check().Supports(Feature.ConvertOperatorBinDataToFromNumeric); + + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.DoubleProperty, new ConvertOptions { ByteOrder = byteOrder, SubType = BsonBinarySubType.Binary })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$DoubleProperty', to : {{ type: 'binData', subtype: 0 }}, {ByteOrderToString(byteOrder)} }} }}, _id : 0 }} }}", + }; + + var expectedResult = new BsonBinaryData(Convert.FromBase64String(expectedBase64)); + AssertOutcome(collection, queryable, expectedStages, expectedResult); + } + + [Theory] + [InlineData(BsonBinarySubType.Binary, 0)] + [InlineData(BsonBinarySubType.Sensitive, 8)] + [InlineData(BsonBinarySubType.UserDefined, 128)] + public void Convert_with_subtype_should_be_rendered_correctly(BsonBinarySubType subType, int expectedSubTypeValue) + { + RequireServer.Check().Supports(Feature.ConvertOperatorBinDataToFromNumeric); + + const int id = 3; + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.DoubleProperty, new ConvertOptions { ByteOrder = ByteOrder.LittleEndian, SubType = subType })); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $convert : {{ input : '$DoubleProperty', to : {{ type: 'binData', subtype: {expectedSubTypeValue} }}, 'byteOrder': 'little' }} }}, _id : 0 }} }}", + }; + + var expectedResult = new BsonBinaryData(Convert.FromBase64String("AAAAAAAA4L8="), subType); + AssertOutcome(collection, queryable, expectedStages, expectedResult); + } + + [Fact] + public void Convert_with_null_options_should_be_reduced_if_possible() + { + const int id = 21; + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.StringProperty, null)); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $toInt : '$StringProperty' }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, 15); + } + + [Fact] + public void Convert_with_empty_options_should_be_reduced_if_possible() + { + const int id = 21; + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.StringProperty, new ConvertOptions())); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $toInt : '$StringProperty' }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, 15); + } + + [Fact] + public void Convert_with_constant_should_work() + { + const int id = 21; + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert("123", new ConvertOptions())); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $toInt : '123' }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, 123); + } + + [Theory] + [InlineData(0, null)] + [InlineData(21, 15)] + public void Convert_to_nullable_value_type_should_work(int id, int? expectedResult) + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(x => Mql.Convert(x.StringProperty, null)); + + var expectedStages = + new[] + { + $"{{ $match : {{ _id : {id} }} }}", + $"{{ $project: {{ _v : {{ $toInt : '$StringProperty' }}, _id : 0 }} }}", + }; + + AssertOutcome(collection, queryable, expectedStages, expectedResult); + } + + public static IEnumerable ConvertTestData => new List + { + // To int + new object[] { 22, (Expression>)(x => Mql.Convert(x.IntProperty, null)), + "{ $project: { _v : { $toInt : '$IntProperty' }, _id : 0 } }", + 33 }, + + // To long + new object[] { 22, (Expression>)(x => Mql.Convert(x.IntProperty, null)), + "{ $project: { _v : { $toLong : '$IntProperty' }, _id : 0 } }", + (long)33 }, + + // To bool + new object[] { 22, (Expression>)(x => Mql.Convert(x.IntProperty, null)), + "{ $project: { _v : { $toBool : '$IntProperty' }, _id : 0 } }", + true }, + + // To decimal + new object[] { 22, (Expression>)(x => Mql.Convert(x.IntProperty, null)), + "{ $project: { _v : { $toDecimal : '$IntProperty' }, _id : 0 } }", + 33m }, + + // To decimal128 + new object[] { 22, (Expression>)(x => Mql.Convert(x.IntProperty, null)), + "{ $project: { _v : { $toDecimal : '$IntProperty' }, _id : 0 } }", + new Decimal128(33) }, + + // To float + new object[] { 22, (Expression>)(x => Mql.Convert(x.IntProperty, null)), + "{ $project: { _v : { $toDouble : '$IntProperty' }, _id : 0 } }", + (float)33.0 }, + + // To double + new object[] { 22, (Expression>)(x => Mql.Convert(x.IntProperty, null)), + "{ $project: { _v : { $toDouble : '$IntProperty' }, _id : 0 } }", + 33.0 }, + + // To objectId + new object[] { 24, (Expression>)(x => Mql.Convert(x.StringProperty, null)), + "{ $project: { _v : { $toObjectId : '$StringProperty' }, _id : 0 } }", + ObjectId.Parse("5ab9cbfa31c2ab715d42129e") }, + + // To date + new object[] { 23, (Expression>)(x => Mql.Convert(x.StringProperty, null)), + "{ $project: { _v : { $toDate : '$StringProperty' }, _id : 0 } }", + new DateTime(2018, 03, 03) }, + + // To string + new object[] { 22, (Expression>)(x => Mql.Convert(x.IntProperty, null)), + "{ $project: { _v : { $toString : '$IntProperty' }, _id : 0 } }", + "33" }, + + // To BsonBinaryData + new object[] { 4, (Expression>)(x => Mql.Convert(x.IntProperty, new ConvertOptions { SubType = BsonBinarySubType.Binary, ByteOrder = ByteOrder.LittleEndian })), + "{ $project: { _v : { $convert : { input : '$IntProperty', to : { type: 'binData', subtype: 0 }, byteOrder : 'little' } }, _id : 0 } }", + new BsonBinaryData(Convert.FromBase64String("ogIAAA==")) }, + + // To byte[] + new object[] { 4, (Expression>)(x => Mql.Convert(x.IntProperty, new ConvertOptions { SubType = BsonBinarySubType.Binary, ByteOrder = ByteOrder.LittleEndian })), + "{ $project: { _v : { $convert : { input : '$IntProperty', to : { type: 'binData', subtype: 0 }, byteOrder : 'little' } }, _id : 0 } }", + Convert.FromBase64String("ogIAAA==") }, + }; + + [Theory] + [MemberData(nameof(ConvertTestData))] + public void Convert_should_work( + int id, + Expression> projection, + string expectedStage, + object expectedValue) + { + if (expectedStage.Contains("byteOrder")) + { + RequireServer.Check().Supports(Feature.ConvertOperatorBinDataToFromNumeric); + } + + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == id) + .Select(projection); + + var expectedStages = new[] { $"{{ $match : {{ _id : {id} }} }}", expectedStage }; + + AssertOutcome(collection, queryable, expectedStages, expectedValue); + } + + [Fact] + public void Convert_should_throw_when_using_unrecognized_to_type() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable() + .Where(x => x.Id == 20) + .Select(x => Mql.Convert("123", new ConvertOptions())); + + var exception = Record.Exception(() => Translate(collection, queryable)); + + Assert.NotNull(exception); + Assert.IsType(exception); + } + + private void AssertOutcome(IMongoCollection collection, + IQueryable queryable, + string[] expectedStages, + TResult expectedResult, + string expectedException = null) + { + TResult result = default; + + var stages = Translate(collection, queryable); + AssertStages(stages, expectedStages); + var exception = Record.Exception(() => result = queryable.Single()); + + if (string.IsNullOrEmpty(expectedException)) + { + Assert.Null(exception); + Assert.Equal(expectedResult, result); + } + else + { + Assert.NotNull(exception); + Assert.Equal(expectedException, exception.GetType().Name); + } + } + + private string ByteOrderToString(ByteOrder byteOrder) + { + var byteOrderString = byteOrder switch + { + ByteOrder.BigEndian => "big", + ByteOrder.LittleEndian => "little", + _ => throw new ArgumentOutOfRangeException(nameof(byteOrder), byteOrder, null) + }; + + return $"byteOrder: '{byteOrderString}'"; + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData => + [ + BsonDocument.Parse("{ _id : 0 }"), + BsonDocument.Parse("{ _id : 1, BinaryProperty : BinData(0, 'ogIAAA==') }"), + BsonDocument.Parse("{ _id : 2, BinaryProperty : BinData(4, 'hn3uUsMxSE6S0cVkebjmfg=='), StringProperty: '867dee52-c331-484e-92d1-c56479b8e67e' }"), + BsonDocument.Parse("{ _id : 3, BinaryProperty : BinData(0, 'AAAAAAAA4L8='), DoubleProperty: -0.5, NullableDoubleProperty: -0.5 }"), // LittleEndian + BsonDocument.Parse("{ _id : 4, BinaryProperty : BinData(0, 'ogIAAA=='), IntProperty: 674, LongProperty: NumberLong('674'), NullableIntProperty: 674, NullableLongProperty: NumberLong('674') }"), // LittleEndian + BsonDocument.Parse("{ _id : 5, BinaryProperty : BinData(0, 'wAQAAAAAAAA='), DoubleProperty: -2.5, NullableDoubleProperty: -2.5 }"), // BigEndian + BsonDocument.Parse("{ _id : 6, BinaryProperty : BinData(0, 'AAAAKg=='), IntProperty: 42, LongProperty: NumberLong('42'), NullableIntProperty: 42, NullableLongProperty: NumberLong('42') }"), // BigEndian + BsonDocument.Parse("{ _id: 10, DoubleProperty: NumberDecimal('-32768'), IntProperty: NumberDecimal('-32768'), LongProperty: NumberDecimal('-32768'), " + + "NullableDoubleProperty: NumberDecimal('-32768'), NullableIntProperty: NumberDecimal('-32768'), NullableLongProperty: NumberDecimal('-32768'), StringProperty: NumberDecimal('-233') }"), // Invalid conversions + BsonDocument.Parse("{ _id : 20, StringProperty: 'inValidInt', IntProperty: 22 }"), + BsonDocument.Parse("{ _id : 21, StringProperty: '15' }"), + BsonDocument.Parse("{ _id : 22, IntProperty: 33 }"), + BsonDocument.Parse("{ _id : 23, StringProperty: '2018-03-03' }"), + BsonDocument.Parse("{ _id : 24, StringProperty: '5ab9cbfa31c2ab715d42129e' }"), + ]; + } + + public class TestClass + { + public int Id { get; set; } + public BsonBinaryData BinaryProperty { get; set; } + public double DoubleProperty { get; set; } + public int IntProperty { get; set; } + public long LongProperty { get; set; } + public string StringProperty { get; set; } + } + } +}