From d63a8de836d8660dcc1f5c10df908c2b32459000 Mon Sep 17 00:00:00 2001 From: rstam Date: Fri, 2 Feb 2024 09:49:10 -0800 Subject: [PATCH] CSHARP-4820: Always use the configured serializer. --- src/MongoDB.Driver/FieldDefinition.cs | 2 +- .../FieldValueSerializerHelper.cs | 62 ++++----- src/MongoDB.Driver/FilterDefinitionBuilder.cs | 6 +- .../Expressions/ISerializationExpression.cs | 4 +- .../LinqProviderAdapterV2.cs | 2 +- .../Translators/PredicateTranslator.cs | 2 +- .../LinqProviderAdapterV3.cs | 2 +- .../Jira/CSharp4820Tests.cs | 118 ++++++++++++++++++ 8 files changed, 161 insertions(+), 37 deletions(-) create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4820Tests.cs diff --git a/src/MongoDB.Driver/FieldDefinition.cs b/src/MongoDB.Driver/FieldDefinition.cs index 5bb86181e4c..c0a9f31d978 100644 --- a/src/MongoDB.Driver/FieldDefinition.cs +++ b/src/MongoDB.Driver/FieldDefinition.cs @@ -422,7 +422,7 @@ public override RenderedFieldDefinition Render( } else if (underlyingSerializer != null) { - valueSerializer = (IBsonSerializer)FieldValueSerializerHelper.GetSerializerForValueType(underlyingSerializer, serializerRegistry, typeof(TField), allowScalarValueForArrayField); + valueSerializer = (IBsonSerializer)FieldValueSerializerHelper.GetSerializerForValueType(underlyingSerializer, serializerRegistry, typeof(TField), allowScalarValueForArrayField, linqProvider); } else { diff --git a/src/MongoDB.Driver/FieldValueSerializerHelper.cs b/src/MongoDB.Driver/FieldValueSerializerHelper.cs index 8a634cd7ea1..0c816266c9a 100644 --- a/src/MongoDB.Driver/FieldValueSerializerHelper.cs +++ b/src/MongoDB.Driver/FieldValueSerializerHelper.cs @@ -20,18 +20,19 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver.Linq; using MongoDB.Driver.Support; namespace MongoDB.Driver { internal static class FieldValueSerializerHelper { - public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSerializer, IBsonSerializerRegistry serializerRegistry, Type valueType) + public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSerializer, IBsonSerializerRegistry serializerRegistry, Type valueType, LinqProvider linqProvider) { - return GetSerializerForValueType(fieldSerializer, serializerRegistry, valueType, allowScalarValueForArrayField: false); + return GetSerializerForValueType(fieldSerializer, serializerRegistry, valueType, allowScalarValueForArrayField: false, linqProvider); } - public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSerializer, IBsonSerializerRegistry serializerRegistry, Type valueType, bool allowScalarValueForArrayField) + public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSerializer, IBsonSerializerRegistry serializerRegistry, Type valueType, bool allowScalarValueForArrayField, LinqProvider linqProvider) { var fieldType = fieldSerializer.ValueType; @@ -48,15 +49,18 @@ public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSer return fieldSerializer; } - // serialize numeric values without converting them - if (fieldType.IsNumeric() && valueType.IsNumeric()) + // serialize numeric values without converting them (only when using LINQ2) + if (linqProvider == LinqProvider.V2) { - var valueSerializer = BsonSerializer.SerializerRegistry.GetSerializer(valueType); - if (HasStringRepresentation(fieldSerializer)) + if (fieldType.IsNumeric() && valueType.IsNumeric()) { - valueSerializer = WithStringRepresentation(valueSerializer); + var valueSerializer = BsonSerializer.SerializerRegistry.GetSerializer(valueType); + if (HasStringRepresentation(fieldSerializer)) + { + valueSerializer = WithStringRepresentation(valueSerializer); + } + return valueSerializer; } - return valueSerializer; } var fieldTypeInfo = fieldType.GetTypeInfo(); @@ -105,21 +109,24 @@ public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSer return (IBsonSerializer)nullableEnumConvertingSerializerConstructor.Invoke(new object[] { nonNullableFieldSerializer }); } - // synthesize an IEnumerableSerializer serializer using the item serializer from the field serializer - Type fieldIEnumerableInterfaceType; - Type valueIEnumerableInterfaceType; - Type itemType; - if ( - (fieldIEnumerableInterfaceType = fieldType.FindIEnumerable()) != null && - (valueIEnumerableInterfaceType = valueType.FindIEnumerable()) != null && - (itemType = fieldIEnumerableInterfaceType.GetSequenceElementType()) == valueIEnumerableInterfaceType.GetSequenceElementType() && - fieldSerializer is IChildSerializerConfigurable) + // synthesize an IEnumerableSerializer serializer using the item serializer from the field serializer (only when using LINQ2) + if (linqProvider == LinqProvider.V2) { - var itemSerializer = ((IChildSerializerConfigurable)fieldSerializer).ChildSerializer; - var itemSerializerInterfaceType = typeof(IBsonSerializer<>).MakeGenericType(itemType); - var ienumerableSerializerType = typeof(IEnumerableSerializer<>).MakeGenericType(itemType); - var ienumerableSerializerConstructor = ienumerableSerializerType.GetTypeInfo().GetConstructor(new[] { itemSerializerInterfaceType }); - return (IBsonSerializer)ienumerableSerializerConstructor.Invoke(new object[] { itemSerializer }); + Type fieldIEnumerableInterfaceType; + Type valueIEnumerableInterfaceType; + Type itemType; + if ( + (fieldIEnumerableInterfaceType = fieldType.FindIEnumerable()) != null && + (valueIEnumerableInterfaceType = valueType.FindIEnumerable()) != null && + (itemType = fieldIEnumerableInterfaceType.GetSequenceElementType()) == valueIEnumerableInterfaceType.GetSequenceElementType() && + fieldSerializer is IChildSerializerConfigurable) + { + var itemSerializer = ((IChildSerializerConfigurable)fieldSerializer).ChildSerializer; + var itemSerializerInterfaceType = typeof(IBsonSerializer<>).MakeGenericType(itemType); + var ienumerableSerializerType = typeof(IEnumerableSerializer<>).MakeGenericType(itemType); + var ienumerableSerializerConstructor = ienumerableSerializerType.GetTypeInfo().GetConstructor(new[] { itemSerializerInterfaceType }); + return (IBsonSerializer)ienumerableSerializerConstructor.Invoke(new object[] { itemSerializer }); + } } if (allowScalarValueForArrayField) @@ -132,7 +139,7 @@ public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSer if (arraySerializer.TryGetItemSerializationInfo(out itemSerializationInfo)) { var itemSerializer = itemSerializationInfo.Serializer; - return GetSerializerForValueType(itemSerializer, serializerRegistry, valueType, allowScalarValueForArrayField: false); + return GetSerializerForValueType(itemSerializer, serializerRegistry, valueType, allowScalarValueForArrayField: false, linqProvider); } } } @@ -141,7 +148,7 @@ public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSer return ConvertIfPossibleSerializer.Create(valueType, fieldType, fieldSerializer, serializerRegistry); } - public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSerializer, IBsonSerializerRegistry serializerRegistry, Type valueType, object value) + public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSerializer, IBsonSerializerRegistry serializerRegistry, Type valueType, object value, LinqProvider linqProvider) { if (!valueType.GetTypeInfo().IsValueType && value == null) { @@ -149,7 +156,7 @@ public static IBsonSerializer GetSerializerForValueType(IBsonSerializer fieldSer } else { - return GetSerializerForValueType(fieldSerializer, serializerRegistry, valueType, allowScalarValueForArrayField: false); + return GetSerializerForValueType(fieldSerializer, serializerRegistry, valueType, allowScalarValueForArrayField: false, linqProvider); } } @@ -216,8 +223,7 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati } else { - var serializer = _serializerRegistry.GetSerializer(); - serializer.Serialize(context, args, value); + throw new InvalidOperationException($"Value could not be converted from type {typeof(TFrom)} to type {typeof(TTo)} to be serialized by the proper serializer."); } } diff --git a/src/MongoDB.Driver/FilterDefinitionBuilder.cs b/src/MongoDB.Driver/FilterDefinitionBuilder.cs index ae0dbc384fb..00bde1982c2 100644 --- a/src/MongoDB.Driver/FilterDefinitionBuilder.cs +++ b/src/MongoDB.Driver/FilterDefinitionBuilder.cs @@ -1813,7 +1813,7 @@ public override BsonDocument Render(IBsonSerializer documentSerialize var message = string.Format("The serializer for field '{0}' must implement IBsonArraySerializer and provide item serialization info.", renderedField.FieldName); throw new InvalidOperationException(message); } - itemSerializer = FieldValueSerializerHelper.GetSerializerForValueType(itemSerializationInfo.Serializer, serializerRegistry, typeof(TItem)); + itemSerializer = FieldValueSerializerHelper.GetSerializerForValueType(itemSerializationInfo.Serializer, serializerRegistry, typeof(TItem), linqProvider); } else { @@ -2445,7 +2445,7 @@ public override BsonDocument Render(IBsonSerializer documentSerialize var message = string.Format("The serializer for field '{0}' must implement IBsonArraySerializer and provide item serialization info.", renderedField.FieldName); throw new InvalidOperationException(message); } - itemSerializer = FieldValueSerializerHelper.GetSerializerForValueType(itemSerializationInfo.Serializer, serializerRegistry, typeof(TItem)); + itemSerializer = FieldValueSerializerHelper.GetSerializerForValueType(itemSerializationInfo.Serializer, serializerRegistry, typeof(TItem), linqProvider); } else { @@ -2494,7 +2494,7 @@ public override BsonDocument Render(IBsonSerializer documentSerialize var message = string.Format("The serializer for field '{0}' must implement IBsonArraySerializer and provide item serialization info.", renderedField.FieldName); throw new InvalidOperationException(message); } - itemSerializer = (IBsonSerializer)FieldValueSerializerHelper.GetSerializerForValueType(itemSerializationInfo.Serializer, serializerRegistry, typeof(TItem)); + itemSerializer = (IBsonSerializer)FieldValueSerializerHelper.GetSerializerForValueType(itemSerializationInfo.Serializer, serializerRegistry, typeof(TItem), linqProvider); } else { diff --git a/src/MongoDB.Driver/Linq/Linq2Implementation/Expressions/ISerializationExpression.cs b/src/MongoDB.Driver/Linq/Linq2Implementation/Expressions/ISerializationExpression.cs index a30c31b203e..4660a81b9ba 100644 --- a/src/MongoDB.Driver/Linq/Linq2Implementation/Expressions/ISerializationExpression.cs +++ b/src/MongoDB.Driver/Linq/Linq2Implementation/Expressions/ISerializationExpression.cs @@ -49,7 +49,7 @@ public static BsonValue SerializeValue(this ISerializationExpression field, Type { Ensure.IsNotNull(field, nameof(field)); - var valueSerializer = FieldValueSerializerHelper.GetSerializerForValueType(field.Serializer, BsonSerializer.SerializerRegistry, valueType, value); + var valueSerializer = FieldValueSerializerHelper.GetSerializerForValueType(field.Serializer, BsonSerializer.SerializerRegistry, valueType, value, LinqProvider.V2); var tempDocument = new BsonDocument(); using (var bsonWriter = new BsonDocumentWriter(tempDocument)) @@ -69,7 +69,7 @@ public static BsonArray SerializeValues(this ISerializationExpression field, Typ Ensure.IsNotNull(itemType, nameof(itemType)); Ensure.IsNotNull(values, nameof(values)); - var itemSerializer = FieldValueSerializerHelper.GetSerializerForValueType(field.Serializer, BsonSerializer.SerializerRegistry, itemType); + var itemSerializer = FieldValueSerializerHelper.GetSerializerForValueType(field.Serializer, BsonSerializer.SerializerRegistry, itemType, LinqProvider.V2); var tempDocument = new BsonDocument(); using (var bsonWriter = new BsonDocumentWriter(tempDocument)) diff --git a/src/MongoDB.Driver/Linq/Linq2Implementation/LinqProviderAdapterV2.cs b/src/MongoDB.Driver/Linq/Linq2Implementation/LinqProviderAdapterV2.cs index 2cf50f115e9..bf0a8687c9c 100644 --- a/src/MongoDB.Driver/Linq/Linq2Implementation/LinqProviderAdapterV2.cs +++ b/src/MongoDB.Driver/Linq/Linq2Implementation/LinqProviderAdapterV2.cs @@ -115,7 +115,7 @@ internal override RenderedFieldDefinition TranslateExpressionToField; - var valueSerializer = (IBsonSerializer)FieldValueSerializerHelper.GetSerializerForValueType(underlyingSerializer, serializerRegistry, typeof(TField), allowScalarValueForArrayField); + var valueSerializer = (IBsonSerializer)FieldValueSerializerHelper.GetSerializerForValueType(underlyingSerializer, serializerRegistry, typeof(TField), allowScalarValueForArrayField, LinqProvider.V2); return new RenderedFieldDefinition(field.FieldName, fieldSerializer, valueSerializer, underlyingSerializer); } diff --git a/src/MongoDB.Driver/Linq/Linq2Implementation/Translators/PredicateTranslator.cs b/src/MongoDB.Driver/Linq/Linq2Implementation/Translators/PredicateTranslator.cs index c3b202203bb..75d381aa28c 100644 --- a/src/MongoDB.Driver/Linq/Linq2Implementation/Translators/PredicateTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq2Implementation/Translators/PredicateTranslator.cs @@ -501,7 +501,7 @@ private FilterDefinition TranslateComparison(Expression variableEx var fieldExpression = GetFieldExpression(variableExpression); - var valueSerializer = FieldValueSerializerHelper.GetSerializerForValueType(fieldExpression.Serializer, _serializerRegistry, constantExpression.Type, value); + var valueSerializer = FieldValueSerializerHelper.GetSerializerForValueType(fieldExpression.Serializer, _serializerRegistry, constantExpression.Type, value, LinqProvider.V2); var serializedValue = valueSerializer.ToBsonValue(value); switch (operatorType) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/LinqProviderAdapterV3.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/LinqProviderAdapterV3.cs index 42628f6f466..3b6a1894101 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/LinqProviderAdapterV3.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/LinqProviderAdapterV3.cs @@ -120,7 +120,7 @@ internal override RenderedFieldDefinition TranslateExpressionToField; - var valueSerializer = (IBsonSerializer)FieldValueSerializerHelper.GetSerializerForValueType(underlyingSerializer, serializerRegistry, typeof(TField), allowScalarValueForArrayField); + var valueSerializer = (IBsonSerializer)FieldValueSerializerHelper.GetSerializerForValueType(underlyingSerializer, serializerRegistry, typeof(TField), allowScalarValueForArrayField, LinqProvider.V3); return new RenderedFieldDefinition(field.Path, fieldSerializer, valueSerializer, underlyingSerializer); } diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4820Tests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4820Tests.cs new file mode 100644 index 00000000000..f01882d1b8b --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4820Tests.cs @@ -0,0 +1,118 @@ +/* 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 FluentAssertions; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver.Linq; +using MongoDB.TestHelpers.XunitExtensions; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira +{ + public class CSharp4820Tests : Linq3IntegrationTest + { + static CSharp4820Tests() + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + var readonlyCollectionMemberMap = cm.GetMemberMap(x => x.ReadOnlyCollection); + var readOnlyCollectionSerializer = readonlyCollectionMemberMap.GetSerializer(); + var bracketingCollectionSerializer = ((IChildSerializerConfigurable)readOnlyCollectionSerializer).WithChildSerializer(new StringBracketingSerializer()); + readonlyCollectionMemberMap.SetSerializer(bracketingCollectionSerializer); + }); + } + + [Theory] + [ParameterAttributeData] + public void Update_Set_with_List_should_work( + [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider) + { + var values = new List() { "abc", "def" }; + var update = Builders.Update.Set(x => x.ReadOnlyCollection, values); + var serializerRegistry = BsonSerializer.SerializerRegistry; + var documentSerializer = serializerRegistry.GetSerializer(); + + var rendered = (BsonDocument)update.Render(documentSerializer, serializerRegistry, linqProvider); + + rendered.Should().Be("{ $set : { ReadOnlyCollection : ['[abc]', '[def]'] } }"); + } + + [Theory] + [ParameterAttributeData] + public void Update_Set_with_Enumerable_should_throw( + [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider) + { + var values = new[] { "abc", "def" }.Select(x => x); + var update = Builders.Update.Set(x => x.ReadOnlyCollection, values); + var serializerRegistry = BsonSerializer.SerializerRegistry; + var documentSerializer = serializerRegistry.GetSerializer(); + + BsonDocument rendered = null; + var exception = Record.Exception(() => rendered = (BsonDocument)update.Render(documentSerializer, serializerRegistry, linqProvider)); + + if (linqProvider == LinqProvider.V2) + { + rendered.Should().Be("{ $set : { ReadOnlyCollection : ['[abc]', '[def]'] } }"); + } + else + { + exception.Should().BeOfType(); + exception.Message.Should().Contain("Value could not be converted"); + } + } + + [Theory] + [ParameterAttributeData] + public void Update_Set_with_Enumerable_ToList_should_work( + [Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider) + { + var values = new[] { "abc", "def" }.Select(x => x); + var update = Builders.Update.Set(x => x.ReadOnlyCollection, values.ToList()); + var serializerRegistry = BsonSerializer.SerializerRegistry; + var documentSerializer = serializerRegistry.GetSerializer(); + + var rendered = (BsonDocument)update.Render(documentSerializer, serializerRegistry, linqProvider); + + rendered.Should().Be("{ $set : { ReadOnlyCollection : ['[abc]', '[def]'] } }"); + } + + private class C + { + public int Id { get; set; } + public IReadOnlyCollection ReadOnlyCollection { get; set; } + } + + private class StringBracketingSerializer : SerializerBase + { + public override string Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var bracketedValue = StringSerializer.Instance.Deserialize(context, args); + return bracketedValue.Substring(1, bracketedValue.Length - 2); + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, string value) + { + var bracketedValue = "[" + value + "]"; + StringSerializer.Instance.Serialize(context, bracketedValue); + } + } + } +}