From 672d248e81b2ca8618f71658ee40241e8e4fe056 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:00:19 +0100 Subject: [PATCH 01/10] =?UTF-8?q?CSHARP-5345:=20Unable=20to=20deserialize?= =?UTF-8?q?=20C#=C2=A0DateOnly=20objects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BsonDateOnlyOptionsAttribute.cs | 81 +++++++++++++++ .../Options/DateOnlyDocumentFormat.cs | 33 +++++++ .../Serializers/DateOnlySerializer.cs | 99 ++++++++++++++++--- 3 files changed, 199 insertions(+), 14 deletions(-) create mode 100644 src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs create mode 100644 src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs diff --git a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs new file mode 100644 index 00000000000..3fda3c9ab61 --- /dev/null +++ b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs @@ -0,0 +1,81 @@ +/* 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.Serialization.Options; +using MongoDB.Bson.Serialization.Serializers; + +namespace MongoDB.Bson.Serialization.Attributes +{ +#if NET6_0_OR_GREATER + /// + /// Specifies the external representation and related options for this field or property. + /// + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] + public class BsonDateOnlyOptionsAttribute : BsonSerializationOptionsAttribute + { + // private fields + private BsonType _representation; + private DateOnlyDocumentFormat _documentFormat; + + // constructors + /// + /// Initializes a new instance of the BsonDateOnlyOptionsAttribute class. + /// + /// The external representation. + public BsonDateOnlyOptionsAttribute(BsonType representation) + { + _representation = representation; + } + + /// + /// Initializes a new instance of the BsonDateOnlyOptionsAttribute class. + /// + /// The external representation. + /// The format to use with document representation. + public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFormat documentDocumentFormat) + { + _representation = representation; + _documentFormat = documentDocumentFormat; + } + + // public properties + /// + /// Gets the external representation. + /// + public BsonType Representation => _representation; + + /// + /// Gets or sets the TimeOnlyUnits. + /// + public DateOnlyDocumentFormat DocumentDocumentFormat => _documentFormat; + + /// + /// Reconfigures the specified serializer by applying this attribute to it. + /// + /// The serializer. + /// A reconfigured serializer. + protected override IBsonSerializer Apply(IBsonSerializer serializer) + { + if (serializer is DateOnlySerializer dateOnlySerializer) + { + return dateOnlySerializer.WithRepresentation(_representation, _documentFormat); + } + + return base.Apply(serializer); + } + } +#endif +} diff --git a/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs b/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs new file mode 100644 index 00000000000..fdf6c1a4000 --- /dev/null +++ b/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs @@ -0,0 +1,33 @@ +/* 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.Options +{ + /// + /// Represents the format to use with a DateOnly serializer when the representation is BsonType.Document. + /// + public enum DateOnlyDocumentFormat + { + /// + /// The document will contain "DateTime" (BsonType.DateTime) and "Ticks" (BsonType.Int64) + /// + Classic, + + /// + /// The document will contain "Year", "Month" and "Day" (all BsonType.Int32) + /// + HumanReadable + } +} \ No newline at end of file diff --git a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs index 85bffdf7e33..4739a4e98cd 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs @@ -15,6 +15,7 @@ using System; using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Options; namespace MongoDB.Bson.Serialization.Serializers @@ -26,7 +27,7 @@ namespace MongoDB.Bson.Serialization.Serializers public sealed class DateOnlySerializer : StructSerializerBase, IRepresentationConfigurable { // static - private static readonly DateOnlySerializer __instance = new DateOnlySerializer(); + private static readonly DateOnlySerializer __instance = new(); /// /// Gets the default DateOnlySerializer. @@ -38,19 +39,23 @@ private static class Flags { public const long DateTime = 1; public const long Ticks = 2; + public const long Year = 3; + public const long Month = 4; + public const long Day = 5; } // private fields private readonly RepresentationConverter _converter; private readonly SerializerHelper _helper; private readonly BsonType _representation; + private readonly DateOnlyDocumentFormat _documentFormat; // constructors /// /// Initializes a new instance of the class. /// public DateOnlySerializer() - : this(BsonType.DateTime) + : this(BsonType.DateTime, DateOnlyDocumentFormat.Classic) { } @@ -59,6 +64,16 @@ public DateOnlySerializer() /// /// The representation. public DateOnlySerializer(BsonType representation) + : this(representation, DateOnlyDocumentFormat.Classic) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The representation. + /// The format to use with the BsonType.Document representation. It will be ignored if the representation is different. + public DateOnlySerializer(BsonType representation, DateOnlyDocumentFormat documentFormat) { switch (representation) { @@ -73,12 +88,16 @@ public DateOnlySerializer(BsonType representation) } _representation = representation; + _documentFormat = documentFormat; _converter = new RepresentationConverter(false, false); _helper = new SerializerHelper ( new SerializerHelper.Member("DateTime", Flags.DateTime), - new SerializerHelper.Member("Ticks", Flags.Ticks) + new SerializerHelper.Member("Ticks", Flags.Ticks), + new SerializerHelper.Member("Year", Flags.Year), + new SerializerHelper.Member("Month", Flags.Month), + new SerializerHelper.Member("Day", Flags.Day) ); } @@ -86,6 +105,11 @@ public DateOnlySerializer(BsonType representation) /// public BsonType Representation => _representation; + /// + /// The format to use for the BsonType.Document representation. It will be ignored if the representation is different. + /// + public DateOnlyDocumentFormat DocumentFormat => _documentFormat; + //public methods /// public override DateOnly Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) @@ -102,17 +126,37 @@ public override DateOnly Deserialize(BsonDeserializationContext context, BsonDes break; case BsonType.Document: - value = default; - _helper.DeserializeMembers(context, (_, flag) => + if (_documentFormat is DateOnlyDocumentFormat.Classic) + { + value = default; + _helper.DeserializeMembers(context, (_, flag) => + { + switch (flag) + { + case Flags.DateTime: bsonReader.SkipValue(); break; // ignore value (use Ticks instead) + case Flags.Ticks: + value = VerifyAndMakeDateOnly(new DateTime(Int64Serializer.Instance.Deserialize(context), DateTimeKind.Utc)); + break; + } + }); + } + else { - switch (flag) + var year = 0; + var month = 0; + var day = 0; + _helper.DeserializeMembers(context, (_, flag) => { - case Flags.DateTime: bsonReader.SkipValue(); break; // ignore value (use Ticks instead) - case Flags.Ticks: - value = VerifyAndMakeDateOnly(new DateTime(Int64Serializer.Instance.Deserialize(context), DateTimeKind.Utc)); - break; - } - }); + switch (flag) + { + case Flags.Year: year = bsonReader.ReadInt32(); break; + case Flags.Month: month = bsonReader.ReadInt32(); break; + case Flags.Day: day = bsonReader.ReadInt32(); break; + } + }); + value = new DateOnly(year, month, day); + } + break; case BsonType.Decimal128: @@ -182,8 +226,17 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati case BsonType.Document: bsonWriter.WriteStartDocument(); - bsonWriter.WriteDateTime("DateTime", millisecondsSinceEpoch); - bsonWriter.WriteInt64("Ticks", utcDateTime.Ticks); + if (_documentFormat is DateOnlyDocumentFormat.Classic) + { + bsonWriter.WriteDateTime("DateTime", millisecondsSinceEpoch); + bsonWriter.WriteInt64("Ticks", utcDateTime.Ticks); + } + else + { + bsonWriter.WriteInt32("Year", value.Year); + bsonWriter.WriteInt32("Month", value.Month); + bsonWriter.WriteInt32("Day", value.Day); + } bsonWriter.WriteEndDocument(); break; @@ -200,6 +253,24 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati } } + /// + /// Returns a serializer that has been reconfigured with the specified representation and document format. + /// + /// The representation. + /// The document format to use with BsonType.Document representation. + /// + /// The reconfigured serializer. + /// + public DateOnlySerializer WithRepresentation(BsonType representation, DateOnlyDocumentFormat documentFormat) + { + if (representation == _representation && documentFormat == _documentFormat) + { + return this; + } + + return new DateOnlySerializer(representation, documentFormat); + } + /// public DateOnlySerializer WithRepresentation(BsonType representation) { From fcc2c0e4a3eb887767c6b99025105f33686f3e73 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:30:46 +0100 Subject: [PATCH 02/10] Added tests --- .../Serializers/DateOnlySerializer.cs | 48 +++++++----- .../Serializers/SerializerHelper.cs | 2 - .../Serializers/DateOnlySerializerTests.cs | 74 +++++++++++++++++++ 3 files changed, 105 insertions(+), 19 deletions(-) diff --git a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs index 4739a4e98cd..977bb32b515 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs @@ -35,13 +35,17 @@ public sealed class DateOnlySerializer : StructSerializerBase, IRepres public static DateOnlySerializer Instance => __instance; // private constants - private static class Flags + private static class ClassicFormatFlags { public const long DateTime = 1; public const long Ticks = 2; - public const long Year = 3; - public const long Month = 4; - public const long Day = 5; + } + + private static class HumanReadableFormatFlags + { + public const long Year = 1; + public const long Month = 2; + public const long Day = 4; } // private fields @@ -91,14 +95,24 @@ public DateOnlySerializer(BsonType representation, DateOnlyDocumentFormat docume _documentFormat = documentFormat; _converter = new RepresentationConverter(false, false); - _helper = new SerializerHelper - ( - new SerializerHelper.Member("DateTime", Flags.DateTime), - new SerializerHelper.Member("Ticks", Flags.Ticks), - new SerializerHelper.Member("Year", Flags.Year), - new SerializerHelper.Member("Month", Flags.Month), - new SerializerHelper.Member("Day", Flags.Day) - ); + if (_documentFormat is DateOnlyDocumentFormat.Classic) + { + _helper = new SerializerHelper + ( + new SerializerHelper.Member("DateTime", ClassicFormatFlags.DateTime), + new SerializerHelper.Member("Ticks", ClassicFormatFlags.Ticks) + ); + } + else + { + _helper = new SerializerHelper + ( + new SerializerHelper.Member("Year", HumanReadableFormatFlags.Year), + new SerializerHelper.Member("Month", HumanReadableFormatFlags.Month), + new SerializerHelper.Member("Day", HumanReadableFormatFlags.Day) + ); + } + } // public properties @@ -133,8 +147,8 @@ public override DateOnly Deserialize(BsonDeserializationContext context, BsonDes { switch (flag) { - case Flags.DateTime: bsonReader.SkipValue(); break; // ignore value (use Ticks instead) - case Flags.Ticks: + case ClassicFormatFlags.DateTime: bsonReader.SkipValue(); break; // ignore value (use Ticks instead) + case ClassicFormatFlags.Ticks: value = VerifyAndMakeDateOnly(new DateTime(Int64Serializer.Instance.Deserialize(context), DateTimeKind.Utc)); break; } @@ -149,9 +163,9 @@ public override DateOnly Deserialize(BsonDeserializationContext context, BsonDes { switch (flag) { - case Flags.Year: year = bsonReader.ReadInt32(); break; - case Flags.Month: month = bsonReader.ReadInt32(); break; - case Flags.Day: day = bsonReader.ReadInt32(); break; + case HumanReadableFormatFlags.Year: year = bsonReader.ReadInt32(); break; + case HumanReadableFormatFlags.Month: month = bsonReader.ReadInt32(); break; + case HumanReadableFormatFlags.Day: day = bsonReader.ReadInt32(); break; } }); value = new DateOnly(year, month, day); diff --git a/src/MongoDB.Bson/Serialization/Serializers/SerializerHelper.cs b/src/MongoDB.Bson/Serialization/Serializers/SerializerHelper.cs index f9e9a6ccd79..81b43b0ce70 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/SerializerHelper.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/SerializerHelper.cs @@ -25,7 +25,6 @@ namespace MongoDB.Bson.Serialization.Serializers public class SerializerHelper { // private fields - private readonly long _allMemberFlags; private readonly long _extraMemberFlag; private readonly Member[] _members; private readonly long _requiredMemberFlags; @@ -52,7 +51,6 @@ public SerializerHelper(params Member[] members) foreach (var member in members) { - _allMemberFlags |= member.Flag; if (!member.IsOptional) { _requiredMemberFlags |= member.Flag; diff --git a/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs index b391a99fcd4..bb42b9067e2 100644 --- a/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs +++ b/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs @@ -19,6 +19,8 @@ using FluentAssertions; using MongoDB.Bson.IO; using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson.Serialization.Options; using MongoDB.Bson.Serialization.Serializers; using MongoDB.TestHelpers.XunitExtensions; using Xunit; @@ -28,6 +30,25 @@ namespace MongoDB.Bson.Tests.Serialization.Serializers #if NET6_0_OR_GREATER public class DateOnlySerializerTests { + [Fact] + public void Attribute_should_set_correct_format() + { + var dateOnly = new DateOnly(2024, 10, 05); + + var testObj = new TestClass + { + ClassicFormat = dateOnly, + HumanFormat = dateOnly, + IgnoredFormat = dateOnly + }; + + var json = testObj.ToJson(); + const string expected = """ + { "ClassicFormat" : { "DateTime" : { "$date" : "2024-10-05T00:00:00Z" }, "Ticks" : 638636832000000000 }, "HumanFormat" : { "Year" : 2024, "Month" : 10, "Day" : 5 }, "IgnoredFormat" : 638636832000000000 } + """; + Assert.Equal(expected, json); + } + [Fact] public void Constructor_with_no_arguments_should_return_expected_result() { @@ -95,6 +116,24 @@ public void Deserialize_should_have_expected_result(string json, string expected result.Should().Be(DateOnly.Parse(expectedResult, CultureInfo.InvariantCulture)); } + [Theory] + [InlineData("""{ "x" : { "Year" : 2024, "Month" : 10, "Day" : 5 } }""","2024-10-05" )] + [InlineData("""{ "x" : { "Year" : 9999, "Month" : 12, "Day" : 31 } }""","9999-12-31" )] + [InlineData("""{ "x" : { "Year" : 1, "Month" : 1, "Day" : 1 } }""","0001-01-01" )] + public void Deserialize_with_human_readable_should_have_expected_result(string json, string expectedResult) + { + var subject = new DateOnlySerializer(BsonType.Document, DateOnlyDocumentFormat.HumanReadable); + + using var reader = new JsonReader(json); + reader.ReadStartDocument(); + reader.ReadName("x"); + var context = BsonDeserializationContext.CreateRoot(reader); + var result = subject.Deserialize(context); + reader.ReadEndDocument(); + + result.Should().Be(DateOnly.Parse(expectedResult, CultureInfo.InvariantCulture)); + } + [Theory] [InlineData("""{ "x" : { "$date" : { "$numberLong" : "1729382410000" } } }""")] [InlineData("""{ "x" : { "$numberLong" : "638649792100000000" } }""")] @@ -220,6 +259,29 @@ public void Serialize_should_have_expected_result(BsonType representation, strin result.Should().Be(expectedResult); } + [Theory] + [InlineData("2024-10-20", """{ "x" : { "Year" : { "$numberInt" : "2024" }, "Month" : { "$numberInt" : "10" }, "Day" : { "$numberInt" : "20" } } }""")] + [InlineData( "0001-01-01", """{ "x" : { "Year" : { "$numberInt" : "1" }, "Month" : { "$numberInt" : "1" }, "Day" : { "$numberInt" : "1" } } }""")] + [InlineData("9999-12-31", """{ "x" : { "Year" : { "$numberInt" : "9999" }, "Month" : { "$numberInt" : "12" }, "Day" : { "$numberInt" : "31" } } }""")] + public void Serialize_human_readable_should_have_expected_result(string valueString, string expectedResult) + { + var subject = new DateOnlySerializer(BsonType.Document, DateOnlyDocumentFormat.HumanReadable); + var value = DateOnly.Parse(valueString, CultureInfo.InvariantCulture); + + using var textWriter = new StringWriter(); + using var writer = new JsonWriter(textWriter, + new JsonWriterSettings { OutputMode = JsonOutputMode.CanonicalExtendedJson }); + + var context = BsonSerializationContext.CreateRoot(writer); + writer.WriteStartDocument(); + writer.WriteName("x"); + subject.Serialize(context, value); + writer.WriteEndDocument(); + var result = textWriter.ToString(); + + result.Should().Be(expectedResult); + } + [Fact] public void Serializer_should_be_registered() { @@ -244,6 +306,18 @@ public void WithRepresentation_should_return_expected_result( result.Should().BeSameAs(subject); } } + + private class TestClass + { + [BsonDateOnlyOptions(BsonType.Document, DateOnlyDocumentFormat.Classic)] + public DateOnly ClassicFormat { get; set; } + + [BsonDateOnlyOptions(BsonType.Document, DateOnlyDocumentFormat.HumanReadable)] + public DateOnly HumanFormat { get; set; } + + [BsonDateOnlyOptions(BsonType.Int64, DateOnlyDocumentFormat.HumanReadable)] + public DateOnly IgnoredFormat { get; set; } + } } #endif } \ No newline at end of file From 70cd4460fafbdea09872f1e7a0e33f5c4ebaf6e0 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 5 Nov 2024 09:38:29 +0100 Subject: [PATCH 03/10] Small corrections --- .../Attributes/BsonDateOnlyOptionsAttribute.cs | 9 +++++---- .../Serialization/Options/DateOnlyDocumentFormat.cs | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs index 3fda3c9ab61..26c88a2baaa 100644 --- a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs +++ b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs @@ -21,7 +21,7 @@ namespace MongoDB.Bson.Serialization.Attributes { #if NET6_0_OR_GREATER /// - /// Specifies the external representation and related options for this field or property. + /// Specifies the external representation and related options for a DateOnly field or property. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] public class BsonDateOnlyOptionsAttribute : BsonSerializationOptionsAttribute @@ -36,8 +36,9 @@ public class BsonDateOnlyOptionsAttribute : BsonSerializationOptionsAttribute /// /// The external representation. public BsonDateOnlyOptionsAttribute(BsonType representation) + : this(representation, DateOnlyDocumentFormat.Classic) { - _representation = representation; + } /// @@ -58,9 +59,9 @@ public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFor public BsonType Representation => _representation; /// - /// Gets or sets the TimeOnlyUnits. + /// Gets the document format. /// - public DateOnlyDocumentFormat DocumentDocumentFormat => _documentFormat; + public DateOnlyDocumentFormat DocumentFormat => _documentFormat; /// /// Reconfigures the specified serializer by applying this attribute to it. diff --git a/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs b/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs index fdf6c1a4000..63b3f8face4 100644 --- a/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs +++ b/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs @@ -21,12 +21,12 @@ namespace MongoDB.Bson.Serialization.Options public enum DateOnlyDocumentFormat { /// - /// The document will contain "DateTime" (BsonType.DateTime) and "Ticks" (BsonType.Int64) + /// The document will contain "DateTime" (BsonType.DateTime) and "Ticks" (BsonType.Int64). /// Classic, /// - /// The document will contain "Year", "Month" and "Day" (all BsonType.Int32) + /// The document will contain "Year", "Month" and "Day" (all BsonType.Int32). /// HumanReadable } From cd71b70b2523a32c5849190b148faefec4a0fb5b Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:44:26 +0100 Subject: [PATCH 04/10] Small fix --- .../Serialization/Serializers/DateOnlySerializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs index 977bb32b515..8fec562c93f 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs @@ -288,7 +288,7 @@ public DateOnlySerializer WithRepresentation(BsonType representation, DateOnlyDo /// public DateOnlySerializer WithRepresentation(BsonType representation) { - return representation == _representation ? this : new DateOnlySerializer(representation); + return representation == _representation ? this : new DateOnlySerializer(representation, _documentFormat); } // explicit interface implementations From c2236841cbaa5c2d40ec5f6266c8829683623704 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:08:03 +0100 Subject: [PATCH 05/10] Corrections --- .../Serializers/DateOnlySerializer.cs | 100 +++++++++--------- .../Serializers/DateOnlySerializerTests.cs | 22 ++++ 2 files changed, 74 insertions(+), 48 deletions(-) diff --git a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs index 8fec562c93f..d9bdce2f7e3 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs @@ -43,9 +43,9 @@ private static class ClassicFormatFlags private static class HumanReadableFormatFlags { - public const long Year = 1; - public const long Month = 2; - public const long Day = 4; + public const long Year = 4; + public const long Month = 8; + public const long Day = 16; } // private fields @@ -94,25 +94,14 @@ public DateOnlySerializer(BsonType representation, DateOnlyDocumentFormat docume _representation = representation; _documentFormat = documentFormat; _converter = new RepresentationConverter(false, false); - - if (_documentFormat is DateOnlyDocumentFormat.Classic) - { - _helper = new SerializerHelper - ( - new SerializerHelper.Member("DateTime", ClassicFormatFlags.DateTime), - new SerializerHelper.Member("Ticks", ClassicFormatFlags.Ticks) - ); - } - else - { - _helper = new SerializerHelper - ( - new SerializerHelper.Member("Year", HumanReadableFormatFlags.Year), - new SerializerHelper.Member("Month", HumanReadableFormatFlags.Month), - new SerializerHelper.Member("Day", HumanReadableFormatFlags.Day) - ); - } - + _helper = new SerializerHelper + ( + new SerializerHelper.Member("DateTime", ClassicFormatFlags.DateTime, isOptional: true), + new SerializerHelper.Member("Ticks", ClassicFormatFlags.Ticks, isOptional: true), + new SerializerHelper.Member("Year", HumanReadableFormatFlags.Year, isOptional: true), + new SerializerHelper.Member("Month", HumanReadableFormatFlags.Month, isOptional: true), + new SerializerHelper.Member("Day", HumanReadableFormatFlags.Day, isOptional: true) + ); } // public properties @@ -140,37 +129,52 @@ public override DateOnly Deserialize(BsonDeserializationContext context, BsonDes break; case BsonType.Document: - if (_documentFormat is DateOnlyDocumentFormat.Classic) + var tickFound = false; + var dateTimeFound = false; + var yearFound = false; + var monthFound = false; + var dayFound = false; + + var tickValue = 0L; + var year = 0; + var month = 0; + var day = 0; + + _helper.DeserializeMembers(context, (_, flag) => { - value = default; - _helper.DeserializeMembers(context, (_, flag) => + switch (flag) { - switch (flag) - { - case ClassicFormatFlags.DateTime: bsonReader.SkipValue(); break; // ignore value (use Ticks instead) - case ClassicFormatFlags.Ticks: - value = VerifyAndMakeDateOnly(new DateTime(Int64Serializer.Instance.Deserialize(context), DateTimeKind.Utc)); - break; - } - }); - } - else + case ClassicFormatFlags.DateTime: + dateTimeFound = true; + bsonReader.SkipValue(); break; // ignore value (use Ticks instead) + case ClassicFormatFlags.Ticks: + tickFound = true; + tickValue = Int64Serializer.Instance.Deserialize(context); + break; + case HumanReadableFormatFlags.Year: + yearFound = true; + year = bsonReader.ReadInt32(); break; + case HumanReadableFormatFlags.Month: + monthFound = true; + month = bsonReader.ReadInt32(); break; + case HumanReadableFormatFlags.Day: + dayFound = true; + day = bsonReader.ReadInt32(); break; + } + }); + + var humanReadableFormatFound = yearFound && monthFound && dayFound; + var classicFormatFound = tickFound && dateTimeFound; + + if ((humanReadableFormatFound && (tickFound || dateTimeFound)) + || (classicFormatFound && (yearFound || monthFound || dayFound))) { - var year = 0; - var month = 0; - var day = 0; - _helper.DeserializeMembers(context, (_, flag) => - { - switch (flag) - { - case HumanReadableFormatFlags.Year: year = bsonReader.ReadInt32(); break; - case HumanReadableFormatFlags.Month: month = bsonReader.ReadInt32(); break; - case HumanReadableFormatFlags.Day: day = bsonReader.ReadInt32(); break; - } - }); - value = new DateOnly(year, month, day); + throw new FormatException("Invalid document format."); } + value = classicFormatFound ? VerifyAndMakeDateOnly(new DateTime(tickValue, DateTimeKind.Utc)) + : new DateOnly(year, month, day); + break; case BsonType.Decimal128: diff --git a/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs index bb42b9067e2..05bac50f770 100644 --- a/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs +++ b/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs @@ -102,6 +102,9 @@ public void Deserialize_should_be_forgiving_of_actual_numeric_types(string json, [InlineData("""{ "x" : { "DateTime" : { "$date" : { "$numberLong" : "1729382400000" } }, "Ticks" : { "$numberLong" : "638649792000000000" } } }""","2024-10-20" )] [InlineData("""{ "x" : { "DateTime" : { "$date" : { "$numberLong" : "-62135596800000" } }, "Ticks" : { "$numberLong" : "0" } } }""","0001-01-01" )] [InlineData("""{ "x" : { "DateTime" : { "$date" : { "$numberLong" : "253402214400000" } }, "Ticks" : { "$numberLong" : "3155378112000000000" } } }""","9999-12-31" )] + [InlineData("""{ "x" : { "Year" : 2024, "Month" : 10, "Day" : 5 } }""","2024-10-05" )] + [InlineData("""{ "x" : { "Year" : 9999, "Month" : 12, "Day" : 31 } }""","9999-12-31" )] + [InlineData("""{ "x" : { "Year" : 1, "Month" : 1, "Day" : 1 } }""","0001-01-01" )] public void Deserialize_should_have_expected_result(string json, string expectedResult) { var subject = new DateOnlySerializer(); @@ -120,6 +123,9 @@ public void Deserialize_should_have_expected_result(string json, string expected [InlineData("""{ "x" : { "Year" : 2024, "Month" : 10, "Day" : 5 } }""","2024-10-05" )] [InlineData("""{ "x" : { "Year" : 9999, "Month" : 12, "Day" : 31 } }""","9999-12-31" )] [InlineData("""{ "x" : { "Year" : 1, "Month" : 1, "Day" : 1 } }""","0001-01-01" )] + [InlineData("""{ "x" : { "DateTime" : { "$date" : { "$numberLong" : "1729382400000" } }, "Ticks" : { "$numberLong" : "638649792000000000" } } }""","2024-10-20" )] + [InlineData("""{ "x" : { "DateTime" : { "$date" : { "$numberLong" : "-62135596800000" } }, "Ticks" : { "$numberLong" : "0" } } }""","0001-01-01" )] + [InlineData("""{ "x" : { "DateTime" : { "$date" : { "$numberLong" : "253402214400000" } }, "Ticks" : { "$numberLong" : "3155378112000000000" } } }""","9999-12-31" )] public void Deserialize_with_human_readable_should_have_expected_result(string json, string expectedResult) { var subject = new DateOnlySerializer(BsonType.Document, DateOnlyDocumentFormat.HumanReadable); @@ -152,6 +158,22 @@ public void Deserialize_should_throw_when_date_has_time(string json) exception.Message.Should().Be("Deserialized value has a non-zero time component."); } + [Theory] + [InlineData("""{ "x" : { "Year": 2024, "DateTime" : { "$date" : { "$numberLong" : "1729382400000" } }, "Ticks" : { "$numberLong" : "638649792100000000" } } }""")] + public void Deserialize_should_throw_when_document_format_is_invalid(string json) + { + var subject = new DateOnlySerializer(); + + using var reader = new JsonReader(json); + reader.ReadStartDocument(); + reader.ReadName("x"); + var context = BsonDeserializationContext.CreateRoot(reader); + + var exception = Record.Exception(() => subject.Deserialize(context)); + exception.Should().BeOfType(); + exception.Message.Should().Be("Invalid document format."); + } + [Fact] public void Equals_null_should_return_false() { From 1f7c307c42348fb7f70be489f750470ad3bf4b93 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:21:26 +0100 Subject: [PATCH 06/10] Various corrections --- .../BsonDateOnlyOptionsAttribute.cs | 19 +---- .../IBsonSerializerExtensions.cs | 31 ++++++++ .../Options/DateOnlyDocumentFormat.cs | 4 +- .../Serializers/DateOnlySerializer.cs | 76 +++++++------------ .../Serializers/DateOnlySerializerTests.cs | 40 +++++++--- 5 files changed, 91 insertions(+), 79 deletions(-) diff --git a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs index 26c88a2baaa..8352751e69f 100644 --- a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs +++ b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs @@ -31,22 +31,13 @@ public class BsonDateOnlyOptionsAttribute : BsonSerializationOptionsAttribute private DateOnlyDocumentFormat _documentFormat; // constructors - /// - /// Initializes a new instance of the BsonDateOnlyOptionsAttribute class. - /// - /// The external representation. - public BsonDateOnlyOptionsAttribute(BsonType representation) - : this(representation, DateOnlyDocumentFormat.Classic) - { - - } /// /// Initializes a new instance of the BsonDateOnlyOptionsAttribute class. /// /// The external representation. /// The format to use with document representation. - public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFormat documentDocumentFormat) + public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFormat documentDocumentFormat = DateOnlyDocumentFormat.DateTimeTicks) { _representation = representation; _documentFormat = documentDocumentFormat; @@ -70,12 +61,8 @@ public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFor /// A reconfigured serializer. protected override IBsonSerializer Apply(IBsonSerializer serializer) { - if (serializer is DateOnlySerializer dateOnlySerializer) - { - return dateOnlySerializer.WithRepresentation(_representation, _documentFormat); - } - - return base.Apply(serializer); + var reconfiguredSerializer = serializer.GetReconfigured(s => s.WithRepresentation(_representation, _documentFormat)); + return reconfiguredSerializer ?? base.Apply(serializer); } } #endif diff --git a/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs b/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs index fd5998c93b3..7aa69cbc038 100644 --- a/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs +++ b/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs @@ -60,6 +60,37 @@ serializer is IHasDiscriminatorConvention hasDiscriminatorConvention ? hasDiscriminatorConvention.DiscriminatorConvention : BsonSerializer.LookupDiscriminatorConvention(serializer.ValueType); + /// + /// 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. + /// + /// An optional predicate to determine if the reconfiguration should be applied to the current serializer. + /// + /// The specific type of serializer to be reconfigured. + /// + /// The reconfigured serializer, or null if no leaf serializer could be reconfigured. + /// + internal static IBsonSerializer GetReconfigured(this IBsonSerializer serializer, Func reconfigure, Func shouldReconfigure = null ) + { + switch (serializer) + { + case IChildSerializerConfigurable childSerializerConfigurable: + { + var childSerializer = childSerializerConfigurable.ChildSerializer; + var reconfiguredChildSerializer = childSerializer.GetReconfigured(reconfigure, shouldReconfigure); + return reconfiguredChildSerializer != null? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null; + } + case T typedSerializer when shouldReconfigure?.Invoke(serializer) ?? true: + return reconfigure(typedSerializer); + default: + return null; + } + } + /// /// Serializes a value. /// diff --git a/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs b/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs index 63b3f8face4..525635d1574 100644 --- a/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs +++ b/src/MongoDB.Bson/Serialization/Options/DateOnlyDocumentFormat.cs @@ -23,11 +23,11 @@ public enum DateOnlyDocumentFormat /// /// The document will contain "DateTime" (BsonType.DateTime) and "Ticks" (BsonType.Int64). /// - Classic, + DateTimeTicks, /// /// The document will contain "Year", "Month" and "Day" (all BsonType.Int32). /// - HumanReadable + YearMonthDay } } \ No newline at end of file diff --git a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs index d9bdce2f7e3..358bf24de8b 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs @@ -35,17 +35,16 @@ public sealed class DateOnlySerializer : StructSerializerBase, IRepres public static DateOnlySerializer Instance => __instance; // private constants - private static class ClassicFormatFlags + private static class Flags { public const long DateTime = 1; public const long Ticks = 2; - } - - private static class HumanReadableFormatFlags - { public const long Year = 4; public const long Month = 8; public const long Day = 16; + + public const long DateTimeTicks = DateTime | Ticks; + public const long YearMonthDay = Year | Month | Day; } // private fields @@ -59,7 +58,7 @@ private static class HumanReadableFormatFlags /// Initializes a new instance of the class. /// public DateOnlySerializer() - : this(BsonType.DateTime, DateOnlyDocumentFormat.Classic) + : this(BsonType.DateTime, DateOnlyDocumentFormat.DateTimeTicks) { } @@ -68,7 +67,7 @@ public DateOnlySerializer() /// /// The representation. public DateOnlySerializer(BsonType representation) - : this(representation, DateOnlyDocumentFormat.Classic) + : this(representation, DateOnlyDocumentFormat.DateTimeTicks) { } @@ -96,11 +95,11 @@ public DateOnlySerializer(BsonType representation, DateOnlyDocumentFormat docume _converter = new RepresentationConverter(false, false); _helper = new SerializerHelper ( - new SerializerHelper.Member("DateTime", ClassicFormatFlags.DateTime, isOptional: true), - new SerializerHelper.Member("Ticks", ClassicFormatFlags.Ticks, isOptional: true), - new SerializerHelper.Member("Year", HumanReadableFormatFlags.Year, isOptional: true), - new SerializerHelper.Member("Month", HumanReadableFormatFlags.Month, isOptional: true), - new SerializerHelper.Member("Day", HumanReadableFormatFlags.Day, isOptional: true) + new SerializerHelper.Member("DateTime", Flags.DateTime, isOptional: true), + new SerializerHelper.Member("Ticks", Flags.Ticks, isOptional: true), + new SerializerHelper.Member("Year", Flags.Year, isOptional: true), + new SerializerHelper.Member("Month", Flags.Month, isOptional: true), + new SerializerHelper.Member("Day", Flags.Day, isOptional: true) ); } @@ -129,53 +128,29 @@ public override DateOnly Deserialize(BsonDeserializationContext context, BsonDes break; case BsonType.Document: - var tickFound = false; - var dateTimeFound = false; - var yearFound = false; - var monthFound = false; - var dayFound = false; - - var tickValue = 0L; + var ticks = 0L; var year = 0; var month = 0; var day = 0; - _helper.DeserializeMembers(context, (_, flag) => + var foundMemberFlags = _helper.DeserializeMembers(context, (_, flag) => { switch (flag) { - case ClassicFormatFlags.DateTime: - dateTimeFound = true; - bsonReader.SkipValue(); break; // ignore value (use Ticks instead) - case ClassicFormatFlags.Ticks: - tickFound = true; - tickValue = Int64Serializer.Instance.Deserialize(context); - break; - case HumanReadableFormatFlags.Year: - yearFound = true; - year = bsonReader.ReadInt32(); break; - case HumanReadableFormatFlags.Month: - monthFound = true; - month = bsonReader.ReadInt32(); break; - case HumanReadableFormatFlags.Day: - dayFound = true; - day = bsonReader.ReadInt32(); break; + case Flags.DateTime: bsonReader.SkipValue(); break; // ignore value (use Ticks instead) + case Flags.Ticks: ticks = Int64Serializer.Instance.Deserialize(context); break; + case Flags.Year: year = Int32Serializer.Instance.Deserialize(context); break; + case Flags.Month: month = Int32Serializer.Instance.Deserialize(context); break; + case Flags.Day: day = Int32Serializer.Instance.Deserialize(context); break; } }); - var humanReadableFormatFound = yearFound && monthFound && dayFound; - var classicFormatFound = tickFound && dateTimeFound; - - if ((humanReadableFormatFound && (tickFound || dateTimeFound)) - || (classicFormatFound && (yearFound || monthFound || dayFound))) + return foundMemberFlags switch { - throw new FormatException("Invalid document format."); - } - - value = classicFormatFound ? VerifyAndMakeDateOnly(new DateTime(tickValue, DateTimeKind.Utc)) - : new DateOnly(year, month, day); - - break; + Flags.DateTimeTicks => VerifyAndMakeDateOnly(new DateTime(ticks, DateTimeKind.Utc)), + Flags.YearMonthDay => new DateOnly(year, month, day), + _ => throw new FormatException("Invalid document format.") + }; case BsonType.Decimal128: value = VerifyAndMakeDateOnly(new DateTime(_converter.ToInt64(bsonReader.ReadDecimal128()), DateTimeKind.Utc)); @@ -222,7 +197,8 @@ public override bool Equals(object obj) return base.Equals(obj) && obj is DateOnlySerializer other && - _representation.Equals(other._representation); + _representation.Equals(other._representation) && + _documentFormat.Equals(other._documentFormat); } /// @@ -244,7 +220,7 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati case BsonType.Document: bsonWriter.WriteStartDocument(); - if (_documentFormat is DateOnlyDocumentFormat.Classic) + if (_documentFormat is DateOnlyDocumentFormat.DateTimeTicks) { bsonWriter.WriteDateTime("DateTime", millisecondsSinceEpoch); bsonWriter.WriteInt64("Ticks", utcDateTime.Ticks); diff --git a/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs index 05bac50f770..a2b499dc810 100644 --- a/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs +++ b/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs @@ -37,14 +37,15 @@ public void Attribute_should_set_correct_format() var testObj = new TestClass { - ClassicFormat = dateOnly, - HumanFormat = dateOnly, - IgnoredFormat = dateOnly + DateTimeTicksFormat = dateOnly, + YearMonthDayFormat = dateOnly, + IgnoredFormat = dateOnly, + ArrayYearMonthDayFormat = [dateOnly, dateOnly] }; var json = testObj.ToJson(); const string expected = """ - { "ClassicFormat" : { "DateTime" : { "$date" : "2024-10-05T00:00:00Z" }, "Ticks" : 638636832000000000 }, "HumanFormat" : { "Year" : 2024, "Month" : 10, "Day" : 5 }, "IgnoredFormat" : 638636832000000000 } + { "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 }] } """; Assert.Equal(expected, json); } @@ -68,6 +69,20 @@ public void Constructor_with_representation_should_return_expected_result( subject.Representation.Should().Be(representation); } + [Theory] + [ParameterAttributeData] + public void Constructor_with_representation_and_format_should_return_expected_result( + [Values(BsonType.DateTime, BsonType.String, BsonType.Int64, BsonType.Document)] + BsonType representation, + [Values(DateOnlyDocumentFormat.DateTimeTicks, DateOnlyDocumentFormat.YearMonthDay)] + DateOnlyDocumentFormat format) + { + var subject = new DateOnlySerializer(representation, format); + + subject.Representation.Should().Be(representation); + subject.DocumentFormat.Should().Be(format); + } + [Theory] [InlineData("""{ "x" : { "$numberDouble" : "638649792000000000" } }""","2024-10-20" )] [InlineData("""{ "x" : { "$numberDecimal" : "638649792000000000" } }""","2024-10-20" )] @@ -128,7 +143,7 @@ public void Deserialize_should_have_expected_result(string json, string expected [InlineData("""{ "x" : { "DateTime" : { "$date" : { "$numberLong" : "253402214400000" } }, "Ticks" : { "$numberLong" : "3155378112000000000" } } }""","9999-12-31" )] public void Deserialize_with_human_readable_should_have_expected_result(string json, string expectedResult) { - var subject = new DateOnlySerializer(BsonType.Document, DateOnlyDocumentFormat.HumanReadable); + var subject = new DateOnlySerializer(BsonType.Document, DateOnlyDocumentFormat.YearMonthDay); using var reader = new JsonReader(json); reader.ReadStartDocument(); @@ -287,7 +302,7 @@ public void Serialize_should_have_expected_result(BsonType representation, strin [InlineData("9999-12-31", """{ "x" : { "Year" : { "$numberInt" : "9999" }, "Month" : { "$numberInt" : "12" }, "Day" : { "$numberInt" : "31" } } }""")] public void Serialize_human_readable_should_have_expected_result(string valueString, string expectedResult) { - var subject = new DateOnlySerializer(BsonType.Document, DateOnlyDocumentFormat.HumanReadable); + var subject = new DateOnlySerializer(BsonType.Document, DateOnlyDocumentFormat.YearMonthDay); var value = DateOnly.Parse(valueString, CultureInfo.InvariantCulture); using var textWriter = new StringWriter(); @@ -331,14 +346,17 @@ public void WithRepresentation_should_return_expected_result( private class TestClass { - [BsonDateOnlyOptions(BsonType.Document, DateOnlyDocumentFormat.Classic)] - public DateOnly ClassicFormat { get; set; } + [BsonDateOnlyOptions(BsonType.Document, DateOnlyDocumentFormat.DateTimeTicks)] + public DateOnly DateTimeTicksFormat { get; set; } - [BsonDateOnlyOptions(BsonType.Document, DateOnlyDocumentFormat.HumanReadable)] - public DateOnly HumanFormat { get; set; } + [BsonDateOnlyOptions(BsonType.Document, DateOnlyDocumentFormat.YearMonthDay)] + public DateOnly YearMonthDayFormat { get; set; } - [BsonDateOnlyOptions(BsonType.Int64, DateOnlyDocumentFormat.HumanReadable)] + [BsonDateOnlyOptions(BsonType.Int64, DateOnlyDocumentFormat.YearMonthDay)] public DateOnly IgnoredFormat { get; set; } + + [BsonDateOnlyOptions(BsonType.Document, DateOnlyDocumentFormat.YearMonthDay)] + public DateOnly[] ArrayYearMonthDayFormat { get; set; } } } #endif From 434baed31bb9e5bf39226e913273ba477bb3a6b5 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:23:22 +0100 Subject: [PATCH 07/10] Small fix --- src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs b/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs index 7aa69cbc038..becaae289ad 100644 --- a/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs +++ b/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs @@ -74,7 +74,7 @@ serializer is IHasDiscriminatorConvention hasDiscriminatorConvention /// /// The reconfigured serializer, or null if no leaf serializer could be reconfigured. /// - internal static IBsonSerializer GetReconfigured(this IBsonSerializer serializer, Func reconfigure, Func shouldReconfigure = null ) + internal static IBsonSerializer GetReconfigured(this IBsonSerializer serializer, Func reconfigure, Func shouldReconfigure = null) { switch (serializer) { From 41ec43ca353ba0d6c1a7a1ddb4316d56d6c8b31a Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:16:38 +0100 Subject: [PATCH 08/10] Small corrections --- .../IBsonSerializerExtensions.cs | 2 +- .../Serializers/DateOnlySerializer.cs | 20 +++++----------- .../Serializers/DateOnlySerializerTests.cs | 23 ++++++++++++++----- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs b/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs index becaae289ad..65935bb02c5 100644 --- a/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs +++ b/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs @@ -70,7 +70,7 @@ serializer is IHasDiscriminatorConvention hasDiscriminatorConvention /// /// An optional predicate to determine if the reconfiguration should be applied to the current serializer. /// - /// The specific type of serializer to be reconfigured. + /// The input type for the reconfigure method. /// /// The reconfigured serializer, or null if no leaf serializer could be reconfigured. /// diff --git a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs index 358bf24de8b..6dceaf65831 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs @@ -117,15 +117,13 @@ public DateOnlySerializer(BsonType representation, DateOnlyDocumentFormat docume public override DateOnly Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) { var bsonReader = context.Reader; - DateOnly value; var bsonType = bsonReader.GetCurrentBsonType(); switch (bsonType) { case BsonType.DateTime: - value = VerifyAndMakeDateOnly(BsonUtils.ToDateTimeFromMillisecondsSinceEpoch(bsonReader.ReadDateTime())); - break; + return VerifyAndMakeDateOnly(BsonUtils.ToDateTimeFromMillisecondsSinceEpoch(bsonReader.ReadDateTime())); case BsonType.Document: var ticks = 0L; @@ -153,30 +151,24 @@ public override DateOnly Deserialize(BsonDeserializationContext context, BsonDes }; case BsonType.Decimal128: - value = VerifyAndMakeDateOnly(new DateTime(_converter.ToInt64(bsonReader.ReadDecimal128()), DateTimeKind.Utc)); - break; + return VerifyAndMakeDateOnly(new DateTime(_converter.ToInt64(bsonReader.ReadDecimal128()), DateTimeKind.Utc)); case BsonType.Double: - value = VerifyAndMakeDateOnly(new DateTime(_converter.ToInt64(bsonReader.ReadDouble()), DateTimeKind.Utc)); - break; + return VerifyAndMakeDateOnly(new DateTime(_converter.ToInt64(bsonReader.ReadDouble()), DateTimeKind.Utc)); case BsonType.Int32: - value = VerifyAndMakeDateOnly(new DateTime(bsonReader.ReadInt32(), DateTimeKind.Utc)); - break; + return VerifyAndMakeDateOnly(new DateTime(bsonReader.ReadInt32(), DateTimeKind.Utc)); case BsonType.Int64: - value = VerifyAndMakeDateOnly(new DateTime(bsonReader.ReadInt64(), DateTimeKind.Utc)); - break; + return VerifyAndMakeDateOnly(new DateTime(bsonReader.ReadInt64(), DateTimeKind.Utc)); case BsonType.String: - value = DateOnly.ParseExact(bsonReader.ReadString(), "yyyy-MM-dd"); - break; + return DateOnly.ParseExact(bsonReader.ReadString(), "yyyy-MM-dd"); default: throw CreateCannotDeserializeFromBsonTypeException(bsonType); } - return value; DateOnly VerifyAndMakeDateOnly(DateTime dt) { diff --git a/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs index a2b499dc810..76b1159261c 100644 --- a/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs +++ b/tests/MongoDB.Bson.Tests/Serialization/Serializers/DateOnlySerializerTests.cs @@ -232,17 +232,28 @@ public void Equals_with_equal_fields_should_return_true() } [Theory] - [InlineData(BsonType.String)] - [InlineData(BsonType.Int64)] - [InlineData(BsonType.Document)] - public void Equals_with_not_equal_fields_should_return_true(BsonType representation) + [ParameterAttributeData] + public void Equals_with_different_representation_and_format_should_return_correct( + [Values(BsonType.DateTime, BsonType.String, BsonType.Int64, BsonType.Document)] + BsonType representation, + [Values(DateOnlyDocumentFormat.DateTimeTicks, DateOnlyDocumentFormat.YearMonthDay)] + DateOnlyDocumentFormat format) { var x = new DateOnlySerializer(); - var y = new DateOnlySerializer(representation); + var y = new DateOnlySerializer(representation, format); var result = x.Equals(y); + var result2 = y.Equals(x); + result.Should().Be(result2); - result.Should().Be(false); + if (representation == BsonType.DateTime && format == DateOnlyDocumentFormat.DateTimeTicks) + { + result.Should().Be(true); + } + else + { + result.Should().Be(false); + } } [Fact] From 1e9f28cbebb7ed661c95c86c9af7456c1b227d3b Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:56:16 +0100 Subject: [PATCH 09/10] Corrections according to PR --- .../BsonDateOnlyOptionsAttribute.cs | 2 +- .../IBsonSerializerExtensions.cs | 31 ------------ .../Serialization/SerializerConfigurator.cs | 50 +++++++++++++++++++ 3 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 src/MongoDB.Bson/Serialization/SerializerConfigurator.cs diff --git a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs index 8352751e69f..6b90edd06fd 100644 --- a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs +++ b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs @@ -61,7 +61,7 @@ public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFor /// A reconfigured serializer. protected override IBsonSerializer Apply(IBsonSerializer serializer) { - var reconfiguredSerializer = serializer.GetReconfigured(s => s.WithRepresentation(_representation, _documentFormat)); + var reconfiguredSerializer = SerializerConfigurator.ReconfigureSerializer(serializer, s => s.WithRepresentation(_representation, _documentFormat)); return reconfiguredSerializer ?? base.Apply(serializer); } } diff --git a/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs b/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs index 65935bb02c5..fd5998c93b3 100644 --- a/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs +++ b/src/MongoDB.Bson/Serialization/IBsonSerializerExtensions.cs @@ -60,37 +60,6 @@ serializer is IHasDiscriminatorConvention hasDiscriminatorConvention ? hasDiscriminatorConvention.DiscriminatorConvention : BsonSerializer.LookupDiscriminatorConvention(serializer.ValueType); - /// - /// 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. - /// - /// An optional predicate to determine if the reconfiguration should be applied to the current serializer. - /// - /// The input type for the reconfigure method. - /// - /// The reconfigured serializer, or null if no leaf serializer could be reconfigured. - /// - internal static IBsonSerializer GetReconfigured(this IBsonSerializer serializer, Func reconfigure, Func shouldReconfigure = null) - { - switch (serializer) - { - case IChildSerializerConfigurable childSerializerConfigurable: - { - var childSerializer = childSerializerConfigurable.ChildSerializer; - var reconfiguredChildSerializer = childSerializer.GetReconfigured(reconfigure, shouldReconfigure); - return reconfiguredChildSerializer != null? childSerializerConfigurable.WithChildSerializer(reconfiguredChildSerializer) : null; - } - case T typedSerializer when shouldReconfigure?.Invoke(serializer) ?? true: - return reconfigure(typedSerializer); - default: - return null; - } - } - /// /// Serializes a value. /// diff --git a/src/MongoDB.Bson/Serialization/SerializerConfigurator.cs b/src/MongoDB.Bson/Serialization/SerializerConfigurator.cs new file mode 100644 index 00000000000..7e1cfbecbe5 --- /dev/null +++ b/src/MongoDB.Bson/Serialization/SerializerConfigurator.cs @@ -0,0 +1,50 @@ +/* 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; + +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) + { + switch (serializer) + { + 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); + + default: + return null; + } + } + } +} \ No newline at end of file From 0d3bc2396735f432e46f94dd6dea744ab96f3f49 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:05:13 -0500 Subject: [PATCH 10/10] Small final correction --- .../Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs index 6b90edd06fd..34ed457ddd8 100644 --- a/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs +++ b/src/MongoDB.Bson/Serialization/Attributes/BsonDateOnlyOptionsAttribute.cs @@ -61,7 +61,7 @@ public BsonDateOnlyOptionsAttribute(BsonType representation, DateOnlyDocumentFor /// A reconfigured serializer. protected override IBsonSerializer Apply(IBsonSerializer serializer) { - var reconfiguredSerializer = SerializerConfigurator.ReconfigureSerializer(serializer, s => s.WithRepresentation(_representation, _documentFormat)); + var reconfiguredSerializer = SerializerConfigurator.ReconfigureSerializer(serializer, (DateOnlySerializer s) => s.WithRepresentation(_representation, _documentFormat)); return reconfiguredSerializer ?? base.Apply(serializer); } }