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);
}
}