From 6747c43059104a4a18049c043484e913efd00e52 Mon Sep 17 00:00:00 2001 From: Jania Vandevoorde <98466424+janiavdv@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:38:01 -0400 Subject: [PATCH] CSHARP-5163: Translate fails when serializing different sized numbers (#1369) * Create unit tests for CSHARP-5163 * CSHARP-5163: Support widening converts in arithmetic expressions. * CSHARP-5163: Use explicit short in test (instead of implied int). --------- Co-authored-by: rstam --- .../Linq3Implementation/Misc/ConvertHelper.cs | 31 +++++++-- .../Misc/SerializationHelper.cs | 18 ++--- ...essionToAggregationExpressionTranslator.cs | 22 +++++- .../Jira/CSharp5163Tests.cs | 68 +++++++++++++++++++ 4 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5163Tests.cs diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ConvertHelper.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ConvertHelper.cs index 6cfec336a72..98e054b954e 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ConvertHelper.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/ConvertHelper.cs @@ -16,6 +16,10 @@ using System; using System.Linq; using System.Linq.Expressions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Options; +using MongoDB.Bson.Serialization.Serializers; namespace MongoDB.Driver.Linq.Linq3Implementation.Misc { @@ -76,6 +80,28 @@ private readonly static (Type SourceType, Type TargetType)[] __wideningConverts (typeof(double), typeof(decimal)) }; + public static IBsonSerializer CreateWiderSerializer(Type narrowerType, Type widerType) + { + if (IsWideningConvert(narrowerType, widerType)) + { + return widerType switch + { + _ when widerType == typeof(int) => new Int32Serializer(BsonType.Int32), + _ when widerType == typeof(long) => new Int64Serializer(BsonType.Int64), + _ when widerType == typeof(decimal) => new DecimalSerializer(BsonType.Decimal128), + _ when widerType == typeof(double) => new DoubleSerializer(BsonType.Double), + _ => throw new ArgumentException($"Cannot create a wider serializer of type {widerType}.", nameof(widerType)) + }; + } + + throw new ArgumentException($"{widerType} is not a wider type for {narrowerType}", nameof(widerType)); + } + + public static bool IsWideningConvert(Type sourceType, Type targetType) + { + return __wideningConverts.Contains((sourceType, targetType)); + } + public static Expression RemoveConvertToMongoQueryable(Expression expression) { if (expression.NodeType == ExpressionType.Convert) @@ -154,11 +180,6 @@ public static Expression RemoveWideningConvert(Expression expression) } return expression; - - static bool IsWideningConvert(Type sourceType, Type targetType) - { - return __wideningConverts.Contains((sourceType, targetType)); - } } } } diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/SerializationHelper.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/SerializationHelper.cs index 414b15c6f9e..bb5a5f4f601 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/SerializationHelper.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Misc/SerializationHelper.cs @@ -42,15 +42,6 @@ public static void EnsureRepresentationIsNumeric(Expression expression, IBsonSer { throw new ExpressionNotSupportedException(expression, because: $"serializer for type {serializer.ValueType} uses a non-numeric representation: {representation}"); } - - static bool IsNumericRepresentation(BsonType representation) - { - return representation switch - { - BsonType.Decimal128 or BsonType.Double or BsonType.Int32 or BsonType.Int64 => true, - _ => false - }; - } } public static BsonType GetRepresentation(IBsonSerializer serializer) @@ -106,6 +97,15 @@ public static BsonType GetRepresentation(IBsonSerializer serializer) return BsonType.Undefined; } + public static bool IsNumericRepresentation(BsonType representation) + { + return representation switch + { + BsonType.Decimal128 or BsonType.Double or BsonType.Int32 or BsonType.Int64 => true, + _ => false + }; + } + public static bool IsRepresentedAsDocument(IBsonSerializer serializer) { return SerializationHelper.GetRepresentation(serializer) == BsonType.Document; diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/BinaryExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/BinaryExpressionToAggregationExpressionTranslator.cs index 94251e3dff4..a8a45caa232 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/BinaryExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/BinaryExpressionToAggregationExpressionTranslator.cs @@ -155,6 +155,23 @@ public static bool AreOperandTypesCompatible(Expression expression, Expression l return false; } + private static IBsonSerializer GetConstantSerializer(BinaryExpression containingExpression, IBsonSerializer otherSerializer, Type constantType) + { + if ( + IsArithmeticExpression(containingExpression) && + otherSerializer.ValueType != constantType && + ConvertHelper.IsWideningConvert(otherSerializer.ValueType, constantType) && + otherSerializer is IRepresentationConfigurable otherRepresentationConfigurableSerializer && + SerializationHelper.IsNumericRepresentation(otherRepresentationConfigurableSerializer.Representation)) + { + return ConvertHelper.CreateWiderSerializer(otherSerializer.ValueType, constantType); + } + else + { + return otherSerializer; + } + } + private static bool IsAddOrSubtractExpression(Expression expression) { return expression.NodeType switch @@ -242,9 +259,10 @@ private static AstBinaryOperator ToBinaryOperator(ExpressionType nodeType) private static AggregationExpression TranslateConstant(BinaryExpression containingExpression, ConstantExpression constantExpression, IBsonSerializer otherSerializer) { - var serializedValue = SerializationHelper.SerializeValue(otherSerializer, constantExpression, containingExpression); + var constantSerializer = GetConstantSerializer(containingExpression, otherSerializer, constantExpression.Type); + var serializedValue = SerializationHelper.SerializeValue(constantSerializer, constantExpression, containingExpression); var ast = AstExpression.Constant(serializedValue); - return new AggregationExpression(constantExpression, ast, otherSerializer); + return new AggregationExpression(constantExpression, ast, constantSerializer); } private static AggregationExpression TranslateEnumExpression(TranslationContext context, BinaryExpression expression) diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5163Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5163Tests.cs new file mode 100644 index 00000000000..4d7f335913e --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp5163Tests.cs @@ -0,0 +1,68 @@ +/* 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 FluentAssertions; +using MongoDB.Driver.Linq; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira; + +public class CSharp5163Tests : Linq3IntegrationTest +{ + [Fact] + public void Select_muliply_int_long_should_work() + { + var collection = GetCollection(); + + var queryable = collection.AsQueryable() + .Select(x => x.Int * 36000000000L); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $multiply : ['$Int', NumberLong('36000000000')] }, _id : 0 } }"); + + var result = queryable.ToList(); + result[0].Should().Be(36000000000L); + } + + [Fact] + public void Select_muliply_byte_short_should_work() + { + var collection = GetCollection(); + + var queryable = collection.AsQueryable() + .Select(x => x.Byte * (short)256); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $multiply : ['$Byte', 256] }, _id : 0 } }"); + + var result = queryable.ToList(); + result[0].Should().Be(256); + } + + private IMongoCollection GetCollection() + { + var collection = GetCollection("test"); + CreateCollection( + collection, + new C { Int = 1, Byte = 1}); + return collection; + } + + private class C + { + public int Int { get; set; } + public byte Byte { get; set; } + } +}