diff --git a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs index 34ed457ddd..351a8fa121 100644 --- a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs +++ b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs @@ -61,8 +61,10 @@ public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFor /// A reconfigured serializer. protected override IBsonSerializer Apply(IBsonSerializer serializer) { - var reconfiguredSerializer = SerializerConfigurator.ReconfigureSerializer(serializer, (DateOnlySerializer s) => s.WithRepresentation(_representation, _documentFormat)); - return reconfiguredSerializer ?? base.Apply(serializer); + return SerializerConfigurator.ReconfigureSerializerRecursively(serializer, Reconfigure) ?? base.Apply(serializer); + + IBsonSerializer Reconfigure(IBsonSerializer s) + => s is DateOnlySerializer dos ? dos.WithRepresentation(_representation, _documentFormat) : null; } } #endif diff --git a/src/MongoDB.Bson/Serialization/Conventions/EnumRepresentationConvention.cs b/src/MongoDB.Bson/Serialization/Conventions/EnumRepresentationConvention.cs index 55ad9a00b9..a57fa18ac5 100644 --- a/src/MongoDB.Bson/Serialization/Conventions/EnumRepresentationConvention.cs +++ b/src/MongoDB.Bson/Serialization/Conventions/EnumRepresentationConvention.cs @@ -14,7 +14,6 @@ */ using System; -using System.Reflection; namespace MongoDB.Bson.Serialization.Conventions { @@ -25,6 +24,7 @@ public class EnumRepresentationConvention : ConventionBase, IMemberMapConvention { // private fields private readonly BsonType _representation; + private readonly bool _topLevelOnly; // constructors /// @@ -33,9 +33,21 @@ public class EnumRepresentationConvention : ConventionBase, IMemberMapConvention /// The serialization representation. 0 is used to detect representation /// from the enum itself. public EnumRepresentationConvention(BsonType representation) + :this(representation, true) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The serialization representation. 0 is used to detect representation + /// from the enum itself. + /// If set to true, the convention will be applied only to top level enum properties, and not collections of enums, for example. + public EnumRepresentationConvention(BsonType representation, bool topLevelOnly) { EnsureRepresentationIsValidForEnums(representation); _representation = representation; + _topLevelOnly = topLevelOnly; } /// @@ -43,66 +55,40 @@ public EnumRepresentationConvention(BsonType representation) /// public BsonType Representation => _representation; + /// + /// Gets a boolean indicating if this convention should be also applied only to the top level enum properties and not to others, + /// collections of enums for example. True by default. + /// + public bool TopLevelOnly => _topLevelOnly; + /// /// Applies a modification to the member map. /// /// The member map. public void Apply(BsonMemberMap memberMap) { - var memberType = memberMap.MemberType; - var memberTypeInfo = memberType.GetTypeInfo(); + var serializer = memberMap.GetSerializer(); + var reconfiguredSerializer = _topLevelOnly && !serializer.ValueType.IsNullableEnum() ? + Reconfigure(serializer) : + SerializerConfigurator.ReconfigureSerializerRecursively(serializer, Reconfigure); - if (memberTypeInfo.IsEnum) + if (reconfiguredSerializer is not null) { - var serializer = memberMap.GetSerializer(); - var representationConfigurableSerializer = serializer as IRepresentationConfigurable; - if (representationConfigurableSerializer != null) - { - var reconfiguredSerializer = representationConfigurableSerializer.WithRepresentation(_representation); - memberMap.SetSerializer(reconfiguredSerializer); - } - return; + memberMap.SetSerializer(reconfiguredSerializer); } - if (IsNullableEnum(memberType)) - { - var serializer = memberMap.GetSerializer(); - var childSerializerConfigurableSerializer = serializer as IChildSerializerConfigurable; - if (childSerializerConfigurableSerializer != null) - { - var childSerializer = childSerializerConfigurableSerializer.ChildSerializer; - var representationConfigurableChildSerializer = childSerializer as IRepresentationConfigurable; - if (representationConfigurableChildSerializer != null) - { - var reconfiguredChildSerializer = representationConfigurableChildSerializer.WithRepresentation(_representation); - var reconfiguredSerializer = childSerializerConfigurableSerializer.WithChildSerializer(reconfiguredChildSerializer); - memberMap.SetSerializer(reconfiguredSerializer); - } - } - return; - } + IBsonSerializer Reconfigure(IBsonSerializer s) + => s.ValueType.IsEnum ? (s as IRepresentationConfigurable)?.WithRepresentation(_representation) : null; } // private methods - private bool IsNullableEnum(Type type) - { - return - type.GetTypeInfo().IsGenericType && - type.GetGenericTypeDefinition() == typeof(Nullable<>) && - Nullable.GetUnderlyingType(type).GetTypeInfo().IsEnum; - } - private void EnsureRepresentationIsValidForEnums(BsonType representation) { - if ( - representation == 0 || - representation == BsonType.String || - representation == BsonType.Int32 || - representation == BsonType.Int64) + if (representation is 0 or BsonType.String or BsonType.Int32 or BsonType.Int64) { return; } - throw new ArgumentException("Enums can only be represented as String, Int32, Int64 or the type of the enum", "representation"); + throw new ArgumentException("Enums can only be represented as String, Int32, Int64 or the type of the enum", nameof(representation)); } } } diff --git a/src/MongoDB.Bson/Serialization/IMultipleChildSerializersConfigurable.cs b/src/MongoDB.Bson/Serialization/IMultipleChildSerializersConfigurable.cs new file mode 100644 index 0000000000..4c9a858ba8 --- /dev/null +++ b/src/MongoDB.Bson/Serialization/IMultipleChildSerializersConfigurable.cs @@ -0,0 +1,38 @@ +/* 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. + */ + +namespace MongoDB.Bson.Serialization +{ + /// + /// Represents a serializer that has multiple child serializers that configuration attributes can be forwarded to. + /// + public interface IMultipleChildSerializersConfigurable + { + /// + /// Gets the child serializers. + /// + /// + /// The child serializers. + /// + IBsonSerializer[] ChildSerializers { get; } + + /// + /// Returns a serializer that has been reconfigured with the specified child serializers. + /// + /// The child serializers. + /// The reconfigured serializer. + IBsonSerializer WithChildSerializers(IBsonSerializer[] childSerializers); + } +} \ No newline at end of file diff --git a/src/MongoDB.Bson/Serialization/SerializerConfigurator.cs b/src/MongoDB.Bson/Serialization/SerializerConfigurator.cs index 7e1cfbecbe..d02062a1a4 100644 --- a/src/MongoDB.Bson/Serialization/SerializerConfigurator.cs +++ b/src/MongoDB.Bson/Serialization/SerializerConfigurator.cs @@ -14,36 +14,45 @@ */ using System; +using System.Collections.Generic; namespace MongoDB.Bson.Serialization { internal static class SerializerConfigurator { - /// - /// Reconfigures a serializer using the specified method. - /// If the serializer implements , - /// the method traverses and applies the reconfiguration to its child serializers recursively until an appropriate leaf serializer is found. - /// - /// The input serializer to be reconfigured. - /// A function that defines how the serializer of type should be reconfigured. - /// The input type for the reconfigure method. - /// - /// The reconfigured serializer, or null if no leaf serializer could be reconfigured. - /// - internal static IBsonSerializer ReconfigureSerializer(IBsonSerializer serializer, Func reconfigure) + // Reconfigures a serializer recursively. + // The reconfigure Func should return null if it does not apply to a given serializer. + internal static IBsonSerializer ReconfigureSerializerRecursively( + IBsonSerializer serializer, + Func reconfigure) { switch (serializer) { + // check IMultipleChildSerializersConfigurableSerializer first because some serializers implement both interfaces + case IMultipleChildSerializersConfigurable multipleChildSerializerConfigurable: + { + var anyChildSerializerWasReconfigured = false; + var reconfiguredChildSerializers = new List(); + + foreach (var childSerializer in multipleChildSerializerConfigurable.ChildSerializers) + { + var reconfiguredChildSerializer = ReconfigureSerializerRecursively(childSerializer, reconfigure); + anyChildSerializerWasReconfigured |= reconfiguredChildSerializer != null; + reconfiguredChildSerializers.Add(reconfiguredChildSerializer ?? childSerializer); + } + + return anyChildSerializerWasReconfigured ? multipleChildSerializerConfigurable.WithChildSerializers(reconfiguredChildSerializers.ToArray()) : null; + } + case IChildSerializerConfigurable childSerializerConfigurable: + { var childSerializer = childSerializerConfigurable.ChildSerializer; - var reconfiguredChildSerializer = ReconfigureSerializer(childSerializer, reconfigure); - return reconfiguredChildSerializer != null? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null; - - case TSerializer typedSerializer: - return reconfigure(typedSerializer); + var reconfiguredChildSerializer = ReconfigureSerializerRecursively(childSerializer, reconfigure); + return reconfiguredChildSerializer != null ? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null; + } default: - return null; + return reconfigure(serializer); } } } diff --git a/src/MongoDB.Bson/Serialization/Serializers/DictionaryInterfaceImplementerSerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/DictionaryInterfaceImplementerSerializer.cs index d188eb1c9f..59e6cb4236 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/DictionaryInterfaceImplementerSerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/DictionaryInterfaceImplementerSerializer.cs @@ -27,6 +27,7 @@ namespace MongoDB.Bson.Serialization.Serializers public sealed class DictionaryInterfaceImplementerSerializer : DictionarySerializerBase, IChildSerializerConfigurable, + IMultipleChildSerializersConfigurable, IDictionaryRepresentationConfigurable where TDictionary : class, IDictionary, new() { @@ -153,6 +154,24 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati { return WithDictionaryRepresentation(dictionaryRepresentation); } + + IBsonSerializer[] IMultipleChildSerializersConfigurable.ChildSerializers => [KeySerializer, ValueSerializer]; + + IBsonSerializer IMultipleChildSerializersConfigurable.WithChildSerializers(IBsonSerializer[] childSerializers) + { + if (childSerializers.Length != 2) + { + throw new Exception("Wrong number of child serializers passed."); + } + + var newKeySerializer = childSerializers[0]; + var newValueSerializer = childSerializers[1]; + + return newKeySerializer.Equals(KeySerializer) && newValueSerializer.Equals(ValueSerializer) + ? this + : new DictionaryInterfaceImplementerSerializer(DictionaryRepresentation, newKeySerializer, + newValueSerializer); + } } /// @@ -164,6 +183,7 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati public class DictionaryInterfaceImplementerSerializer : DictionarySerializerBase, IChildSerializerConfigurable, + IMultipleChildSerializersConfigurable, IDictionaryRepresentationConfigurable> where TDictionary : class, IDictionary { @@ -281,6 +301,24 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati return WithDictionaryRepresentation(dictionaryRepresentation); } + IBsonSerializer[] IMultipleChildSerializersConfigurable.ChildSerializers => [KeySerializer, ValueSerializer]; + + IBsonSerializer IMultipleChildSerializersConfigurable.WithChildSerializers(IBsonSerializer[] childSerializers) + { + if (childSerializers.Length != 2) + { + throw new Exception("Wrong number of child serializers passed."); + } + + var newKeySerializer = (IBsonSerializer)childSerializers[0]; + var newValueSerializer = (IBsonSerializer)childSerializers[1]; + + return newKeySerializer.Equals(KeySerializer) && newValueSerializer.Equals(ValueSerializer) + ? this + : new DictionaryInterfaceImplementerSerializer(DictionaryRepresentation, newKeySerializer, + newValueSerializer); + } + /// protected override ICollection> CreateAccumulator() { diff --git a/src/MongoDB.Bson/Serialization/Serializers/ReadOnlyDictionaryInterfaceImplementerSerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/ReadOnlyDictionaryInterfaceImplementerSerializer.cs index ae2b747922..4d522ca580 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/ReadOnlyDictionaryInterfaceImplementerSerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/ReadOnlyDictionaryInterfaceImplementerSerializer.cs @@ -28,6 +28,7 @@ namespace MongoDB.Bson.Serialization.Serializers public sealed class ReadOnlyDictionaryInterfaceImplementerSerializer : DictionarySerializerBase, IChildSerializerConfigurable, + IMultipleChildSerializersConfigurable, IDictionaryRepresentationConfigurable> where TDictionary : class, IReadOnlyDictionary { @@ -122,6 +123,24 @@ IBsonSerializer IDictionaryRepresentationConfigurable.WithDictionaryRepresentati return WithDictionaryRepresentation(dictionaryRepresentation); } + IBsonSerializer[] IMultipleChildSerializersConfigurable.ChildSerializers => [KeySerializer, ValueSerializer]; + + IBsonSerializer IMultipleChildSerializersConfigurable.WithChildSerializers(IBsonSerializer[] childSerializers) + { + if (childSerializers.Length != 2) + { + throw new Exception("Wrong number of child serializers passed."); + } + + var newKeySerializer = (IBsonSerializer)childSerializers[0]; + var newValueSerializer = (IBsonSerializer)childSerializers[1]; + + return newKeySerializer.Equals(KeySerializer) && newValueSerializer.Equals(ValueSerializer) + ? this + : new ReadOnlyDictionaryInterfaceImplementerSerializer(DictionaryRepresentation, newKeySerializer, + newValueSerializer); + } + /// protected override ICollection> CreateAccumulator() { diff --git a/src/MongoDB.Bson/Serialization/TypeExtensions.cs b/src/MongoDB.Bson/Serialization/TypeExtensions.cs index 04f533ede9..83e7c48d20 100644 --- a/src/MongoDB.Bson/Serialization/TypeExtensions.cs +++ b/src/MongoDB.Bson/Serialization/TypeExtensions.cs @@ -29,5 +29,17 @@ public static bool IsAnonymousType(this Type type) type.IsGenericType && type.Name.Contains("Anon"); // don't check for more than "Anon" so it works in mono also } + + public static bool IsNullable(this Type type) + { + return type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + + public static bool IsNullableEnum(this Type type) + { + return + type.IsNullable() && + Nullable.GetUnderlyingType(type).IsEnum; + } } } diff --git a/tests/MongoDB.Bson.Tests/Serialization/Conventions/EnumRepresentationConventionTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Conventions/EnumRepresentationConventionTests.cs index bde58d6af7..bdba3f1d33 100644 --- a/tests/MongoDB.Bson.Tests/Serialization/Conventions/EnumRepresentationConventionTests.cs +++ b/tests/MongoDB.Bson.Tests/Serialization/Conventions/EnumRepresentationConventionTests.cs @@ -14,11 +14,13 @@ */ using System; +using System.Collections.Generic; using System.Linq.Expressions; using FluentAssertions; using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Conventions; using MongoDB.Bson.Serialization.Serializers; +using MongoDB.TestHelpers.XunitExtensions; using Xunit; namespace MongoDB.Bson.Tests.Serialization.Conventions @@ -31,7 +33,14 @@ public class C { public E E { get; set; } public E? NE { get; set; } + public E[] ArrayEnum { get; set; } + public E[][] ArrayOfArrayEnum { get; set; } + public Dictionary DictionaryEnum { get; set; } + public Dictionary NestedDictionaryEnum { get; set; } + public Dictionary DictionaryKeyEnum { get; set; } public int I { get; set; } + public int NI { get; set; } + public int[] ArrayInt { get; set; } } [Theory] @@ -51,6 +60,7 @@ public void Apply_should_configure_serializer_when_member_is_an_enum(BsonType re [Theory] [InlineData(BsonType.Int32)] [InlineData(BsonType.Int64)] + [InlineData(BsonType.String)] public void Apply_should_configure_serializer_when_member_is_a_nullable_enum(BsonType representation) { var subject = new EnumRepresentationConvention(representation); @@ -63,6 +73,101 @@ public void Apply_should_configure_serializer_when_member_is_a_nullable_enum(Bso childSerializer.Representation.Should().Be(representation); } + [Theory] + [InlineData(BsonType.Int32)] + [InlineData(BsonType.Int64)] + [InlineData(BsonType.String)] + public void Apply_should_configure_serializer_when_member_is_an_enum_collection(BsonType representation) + { + var subject = new EnumRepresentationConvention(representation, false); + var memberMap = CreateMemberMap(c => c.ArrayEnum); + + subject.Apply(memberMap); + + var serializer = (IChildSerializerConfigurable)memberMap.GetSerializer(); + var childSerializer = (EnumSerializer)serializer.ChildSerializer; + childSerializer.Representation.Should().Be(representation); + } + + [Theory] + [InlineData(BsonType.Int32)] + [InlineData(BsonType.Int64)] + [InlineData(BsonType.String)] + public void Apply_should_configure_serializer_when_member_is_a_nested_enum_collection(BsonType representation) + { + var subject = new EnumRepresentationConvention(representation, false); + var memberMap = CreateMemberMap(c => c.ArrayOfArrayEnum); + + subject.Apply(memberMap); + + var serializer = (IChildSerializerConfigurable)memberMap.GetSerializer(); + var childSerializer = (EnumSerializer)((IChildSerializerConfigurable)serializer.ChildSerializer).ChildSerializer; + childSerializer.Representation.Should().Be(representation); + } + + [Theory] + [InlineData(BsonType.Int32)] + [InlineData(BsonType.Int64)] + [InlineData(BsonType.String)] + public void Apply_should_configure_serializer_when_member_is_an_enum_dictionary(BsonType representation) + { + var subject = new EnumRepresentationConvention(representation, false); + var memberMap = CreateMemberMap(c => c.DictionaryEnum); + + subject.Apply(memberMap); + + var serializer = (IChildSerializerConfigurable)memberMap.GetSerializer(); + var childSerializer = (EnumSerializer)serializer.ChildSerializer; + childSerializer.Representation.Should().Be(representation); + } + + [Theory] + [InlineData(BsonType.Int32)] + [InlineData(BsonType.Int64)] + [InlineData(BsonType.String)] + public void Apply_should_configure_serializer_when_member_is_an_enum_dictionary_key(BsonType representation) + { + var subject = new EnumRepresentationConvention(representation, false); + var memberMap = CreateMemberMap(c => c.DictionaryKeyEnum); + + subject.Apply(memberMap); + + var serializer = (IMultipleChildSerializersConfigurable)memberMap.GetSerializer(); + var childSerializer = (EnumSerializer)serializer.ChildSerializers[0]; + childSerializer.Representation.Should().Be(representation); + } + + [Theory] + [InlineData(BsonType.Int32)] + [InlineData(BsonType.Int64)] + [InlineData(BsonType.String)] + public void Apply_should_configure_serializer_when_member_is_an_enum_dictionary_value(BsonType representation) + { + var subject = new EnumRepresentationConvention(representation, false); + var memberMap = CreateMemberMap(c => c.NestedDictionaryEnum); + + subject.Apply(memberMap); + + var serializer = (IChildSerializerConfigurable)memberMap.GetSerializer(); + var childSerializer = (EnumSerializer)((IChildSerializerConfigurable)serializer.ChildSerializer).ChildSerializer; + childSerializer.Representation.Should().Be(representation); + } + + [Theory] + [InlineData(BsonType.Int64)] + [InlineData(BsonType.String)] + public void Apply_should_do_nothing_when_member_is_an_enum_collection_and_top_level_only_is_true(BsonType representation) + { + var subject = new EnumRepresentationConvention(representation, true); + var memberMap = CreateMemberMap(c => c.ArrayEnum); + + subject.Apply(memberMap); + + var serializer = (IChildSerializerConfigurable)memberMap.GetSerializer(); + var childSerializer = (EnumSerializer)serializer.ChildSerializer; + childSerializer.Representation.Should().Be(BsonType.Int32); + } + [Fact] public void Apply_should_do_nothing_when_member_is_not_an_enum() { @@ -75,18 +180,54 @@ public void Apply_should_do_nothing_when_member_is_not_an_enum() memberMap.GetSerializer().Should().BeSameAs(serializer); } + [Fact] + public void Apply_should_do_nothing_when_member_is_not_an_enum_and_nullable() + { + var subject = new EnumRepresentationConvention(BsonType.String); + var memberMap = CreateMemberMap(c => c.NI); + var serializer = memberMap.GetSerializer(); + + subject.Apply(memberMap); + + memberMap.GetSerializer().Should().BeSameAs(serializer); + } + + [Fact] + public void Apply_should_do_nothing_when_member_is_not_an_enum_collection() + { + var subject = new EnumRepresentationConvention(BsonType.String); + var memberMap = CreateMemberMap(c => c.ArrayInt); + var serializer = memberMap.GetSerializer(); + + subject.Apply(memberMap); + + memberMap.GetSerializer().Should().BeSameAs(serializer); + } + [Theory] [InlineData((BsonType)0)] [InlineData(BsonType.Int32)] [InlineData(BsonType.Int64)] [InlineData(BsonType.String)] - public void constructor_should_initialize_instance_when_representation_is_valid(BsonType representation) + public void constructor_with_representation_should_return_expected_result(BsonType representation) { var subject = new EnumRepresentationConvention(representation); subject.Representation.Should().Be(representation); } + [Theory] + [ParameterAttributeData] + public void constructor_with_representation_and_should_apply_to_collection_should_return_expected_result( + [Values((BsonType)0, BsonType.Int32, BsonType.Int64, BsonType.String)] BsonType representation, + [Values(true, false)] bool topLevelOnly) + { + var subject = new EnumRepresentationConvention(representation, topLevelOnly); + + subject.Representation.Should().Be(representation); + subject.TopLevelOnly.Should().Be(topLevelOnly); + } + [Theory] [InlineData(BsonType.Decimal128)] [InlineData(BsonType.Double)] diff --git a/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs index 76b1159261..e4c4eb50e9 100644 --- a/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs +++ b/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs @@ -40,12 +40,13 @@ public void Attribute_should_set_correct_format() DateTimeTicksFormat = dateOnly, YearMonthDayFormat = dateOnly, IgnoredFormat = dateOnly, + NullableYearMonthDayFormat = dateOnly, ArrayYearMonthDayFormat = [dateOnly, dateOnly] }; var json = testObj.ToJson(); const string expected = """ - { "DateTimeTicksFormat" : { "DateTime" : { "$date" : "2024-10-05T00:00:00Z" }, "Ticks" : 638636832000000000 }, "YearMonthDayFormat" : { "Year" : 2024, "Month" : 10, "Day" : 5 }, "IgnoredFormat" : 638636832000000000, "ArrayYearMonthDayFormat" : [{ "Year" : 2024, "Month" : 10, "Day" : 5 }, { "Year" : 2024, "Month" : 10, "Day" : 5 }] } + { "DateTimeTicksFormat" : { "DateTime" : { "$date" : "2024-10-05T00:00:00Z" }, "Ticks" : 638636832000000000 }, "YearMonthDayFormat" : { "Year" : 2024, "Month" : 10, "Day" : 5 }, "IgnoredFormat" : 638636832000000000, "NullableYearMonthDayFormat" : { "Year" : 2024, "Month" : 10, "Day" : 5 }, "ArrayYearMonthDayFormat" : [{ "Year" : 2024, "Month" : 10, "Day" : 5 }, { "Year" : 2024, "Month" : 10, "Day" : 5 }] } """; Assert.Equal(expected, json); } @@ -366,6 +367,9 @@ private class TestClass [BsonDateOnlyOptions(BsonType.Int64, DateOnlyDocumentFormat.YearMonthDay)] public DateOnly IgnoredFormat { get; set; } + [BsonDateOnlyOptions(BsonType.Document, DateOnlyDocumentFormat.YearMonthDay)] + public DateOnly? NullableYearMonthDayFormat { get; set; } + [BsonDateOnlyOptions(BsonType.Document, DateOnlyDocumentFormat.YearMonthDay)] public DateOnly[] ArrayYearMonthDayFormat { get; set; } }