diff --git a/src/MongoDB.Bson/Serialization/Attributes/BsonTimeOnlyOptionsAttribute.cs b/src/MongoDB.Bson/Serialization/Attributes/BsonTimeOnlyOptionsAttribute.cs
index 23f327f3290..0593b3661db 100644
--- a/src/MongoDB.Bson/Serialization/Attributes/BsonTimeOnlyOptionsAttribute.cs
+++ b/src/MongoDB.Bson/Serialization/Attributes/BsonTimeOnlyOptionsAttribute.cs
@@ -44,7 +44,7 @@ public BsonTimeOnlyOptionsAttribute(BsonType representation)
/// Initializes a new instance of the BsonTimeOnlyOptionsAttribute class.
///
/// The external representation.
- /// The TimeOnlyUnits.
+ /// The TimeOnlyUnits. Ignored if representation is BsonType.Document.
public BsonTimeOnlyOptionsAttribute(BsonType representation, TimeOnlyUnits units)
{
_representation = representation;
diff --git a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs
index 6dceaf65831..bb571238b86 100644
--- a/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs
+++ b/src/MongoDB.Bson/Serialization/Serializers/DateOnlySerializer.cs
@@ -15,7 +15,6 @@
using System;
using MongoDB.Bson.IO;
-using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.Options;
namespace MongoDB.Bson.Serialization.Serializers
diff --git a/src/MongoDB.Bson/Serialization/Serializers/TimeOnlySerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/TimeOnlySerializer.cs
index 8b765217005..c902432e3e1 100644
--- a/src/MongoDB.Bson/Serialization/Serializers/TimeOnlySerializer.cs
+++ b/src/MongoDB.Bson/Serialization/Serializers/TimeOnlySerializer.cs
@@ -14,6 +14,7 @@
*/
using System;
+using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization.Options;
namespace MongoDB.Bson.Serialization.Serializers
@@ -32,7 +33,20 @@ public sealed class TimeOnlySerializer: StructSerializerBase, IReprese
///
public static TimeOnlySerializer Instance => __instance;
+ // private constants
+ private static class Flags
+ {
+ public const long Hour = 1;
+ public const long Minute = 2;
+ public const long Second = 4;
+ public const long Millisecond = 8;
+ public const long Microsecond = 16;
+ public const long Nanosecond = 32;
+ public const long Ticks = 64;
+ }
+
// private fields
+ private readonly SerializerHelper _helper;
private readonly BsonType _representation;
private readonly TimeOnlyUnits _units;
@@ -58,11 +72,12 @@ public TimeOnlySerializer(BsonType representation)
/// Initializes a new instance of the class.
///
/// The representation.
- /// The units.
+ /// The units. Ignored if representation is BsonType.Document.
public TimeOnlySerializer(BsonType representation, TimeOnlyUnits units)
{
switch (representation)
{
+ case BsonType.Document:
case BsonType.Double:
case BsonType.Int32:
case BsonType.Int64:
@@ -75,6 +90,20 @@ public TimeOnlySerializer(BsonType representation, TimeOnlyUnits units)
_representation = representation;
_units = units;
+
+ _helper = new SerializerHelper
+ (
+ // TimeOnlySerializer was introduced in version 3.0.0 of the driver. Prior to that, TimeOnly was serialized
+ // as a class mapped POCO. Due to that, Microsecond and Nanosecond could be missing, as they were introduced in .NET 7.
+ // To avoid deserialization issues, we treat Microsecond and Nanosecond as optional members.
+ new SerializerHelper.Member("Hour", Flags.Hour, isOptional: false),
+ new SerializerHelper.Member("Minute", Flags.Minute, isOptional: false),
+ new SerializerHelper.Member("Second", Flags.Second, isOptional: false),
+ new SerializerHelper.Member("Millisecond", Flags.Millisecond, isOptional: false),
+ new SerializerHelper.Member("Microsecond", Flags.Microsecond, isOptional: true),
+ new SerializerHelper.Member("Nanosecond", Flags.Nanosecond, isOptional: true),
+ new SerializerHelper.Member("Ticks", Flags.Ticks, isOptional: false)
+ );
}
// public properties
@@ -98,10 +127,11 @@ public override TimeOnly Deserialize(BsonDeserializationContext context, BsonDes
return bsonType switch
{
- BsonType.String => TimeOnly.ParseExact(bsonReader.ReadString(), "o"),
- BsonType.Int64 => FromInt64(bsonReader.ReadInt64(), _units),
- BsonType.Int32 => FromInt32(bsonReader.ReadInt32(), _units),
+ BsonType.Document => FromDocument(context),
BsonType.Double => FromDouble(bsonReader.ReadDouble(), _units),
+ BsonType.Int32 => FromInt32(bsonReader.ReadInt32(), _units),
+ BsonType.Int64 => FromInt64(bsonReader.ReadInt64(), _units),
+ BsonType.String => TimeOnly.ParseExact(bsonReader.ReadString(), "o"),
_ => throw CreateCannotDeserializeFromBsonTypeException(bsonType)
};
}
@@ -129,6 +159,19 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati
switch (_representation)
{
+ case BsonType.Document:
+ bsonWriter.WriteStartDocument();
+ bsonWriter.WriteInt32("Hour", value.Hour);
+ bsonWriter.WriteInt32("Minute", value.Minute);
+ bsonWriter.WriteInt32("Second", value.Second);
+ bsonWriter.WriteInt32("Millisecond", value.Millisecond);
+ // Microsecond and Nanosecond properties were added in .NET 7
+ bsonWriter.WriteInt32("Microsecond", GetMicrosecondsComponent(value.Ticks));
+ bsonWriter.WriteInt32("Nanosecond", GetNanosecondsComponent(value.Ticks));
+ bsonWriter.WriteInt64("Ticks", value.Ticks);
+ bsonWriter.WriteEndDocument();
+ break;
+
case BsonType.Double:
bsonWriter.WriteDouble(ToDouble(value, _units));
break;
@@ -175,6 +218,60 @@ public TimeOnlySerializer WithRepresentation(BsonType representation, TimeOnlyUn
}
// private methods
+ private TimeOnly FromDocument(BsonDeserializationContext context)
+ {
+ var bsonReader = context.Reader;
+ var hour = 0;
+ var minute = 0;
+ var second = 0;
+ var millisecond = 0;
+ int? microsecond = null;
+ int? nanosecond = null;
+ var ticks = 0L;
+
+ _helper.DeserializeMembers(context, (_, flag) =>
+ {
+ switch (flag)
+ {
+ case Flags.Hour:
+ hour = bsonReader.ReadInt32();
+ break;
+ case Flags.Minute:
+ minute = bsonReader.ReadInt32();
+ break;
+ case Flags.Second:
+ second = bsonReader.ReadInt32();
+ break;
+ case Flags.Millisecond:
+ millisecond = bsonReader.ReadInt32();
+ break;
+ case Flags.Microsecond:
+ microsecond = bsonReader.ReadInt32();
+ break;
+ case Flags.Nanosecond:
+ nanosecond = bsonReader.ReadInt32();
+ break;
+ case Flags.Ticks:
+ ticks = bsonReader.ReadInt64();
+ break;
+ }
+ });
+
+ var deserializedTimeOnly = new TimeOnly(ticks);
+
+ if (deserializedTimeOnly.Hour != hour ||
+ deserializedTimeOnly.Minute != minute ||
+ deserializedTimeOnly.Second != second ||
+ deserializedTimeOnly.Millisecond != millisecond ||
+ (microsecond.HasValue && GetMicrosecondsComponent(deserializedTimeOnly.Ticks) != microsecond.Value) ||
+ (nanosecond.HasValue && GetNanosecondsComponent(deserializedTimeOnly.Ticks) != nanosecond.Value))
+ {
+ throw new BsonSerializationException("Deserialized TimeOnly components do not match the ticks value.");
+ }
+
+ return deserializedTimeOnly;
+ }
+
private TimeOnly FromDouble(double value, TimeOnlyUnits units)
{
return units is TimeOnlyUnits.Nanoseconds
@@ -196,6 +293,18 @@ private TimeOnly FromInt64(long value, TimeOnlyUnits units)
: new TimeOnly(value * TicksPerUnit(units));
}
+ private int GetMicrosecondsComponent(long ticks)
+ {
+ var ticksPerMicrosecond = TicksPerUnit(TimeOnlyUnits.Microseconds);
+ return (int)(ticks / ticksPerMicrosecond % 1000);
+ }
+
+ private int GetNanosecondsComponent(long ticks)
+ {
+ var nanosecondsPerTick = 100;
+ return (int)(ticks * nanosecondsPerTick % 1000);
+ }
+
private long TicksPerUnit(TimeOnlyUnits units)
{
return units switch
diff --git a/tests/MongoDB.Bson.Tests/Serialization/Serializers/TimeOnlySerializerTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Serializers/TimeOnlySerializerTests.cs
index ea344347f56..0ecfb4a56c5 100644
--- a/tests/MongoDB.Bson.Tests/Serialization/Serializers/TimeOnlySerializerTests.cs
+++ b/tests/MongoDB.Bson.Tests/Serialization/Serializers/TimeOnlySerializerTests.cs
@@ -43,17 +43,21 @@ public void Attribute_should_set_correct_units()
Microseconds = timeOnly,
Ticks = timeOnly,
Nanoseconds = timeOnly,
+ Document = timeOnly
};
var json = testObj.ToJson();
- var expected = "{ \"Hours\" : 13, "
- + "\"Minutes\" : 804, "
- + "\"Seconds\" : 48293, "
- + "\"Milliseconds\" : 48293000, "
- + "\"Microseconds\" : 48293000000, "
- + "\"Ticks\" : 482930000000, "
- + "\"Nanoseconds\" : 48293000000000 }";
+ var baseString = """
+ { "Hours" : 13, "Minutes" : 804, "Seconds" : 48293, "Milliseconds" : 48293000, "Microseconds" : 48293000000, "Ticks" : 482930000000, "Nanoseconds" : 48293000000000
+ """;
+
+ var documentString = """
+ { "Hour" : 13, "Minute" : 24, "Second" : 53, "Millisecond" : 0, "Microsecond" : 0, "Nanosecond" : 0, "Ticks" : 482930000000 }
+ """;
+
+
+ var expected = baseString + """, "Document" : """ + documentString + " }";
Assert.Equal(expected, json);
}
@@ -69,7 +73,7 @@ public void Constructor_with_no_arguments_should_return_expected_result()
[Theory]
[ParameterAttributeData]
public void Constructor_with_representation_should_return_expected_result(
- [Values(BsonType.String, BsonType.Int64, BsonType.Int32, BsonType.Double)]
+ [Values(BsonType.String, BsonType.Int64, BsonType.Int32, BsonType.Double, BsonType.Document)]
BsonType representation,
[Values(TimeOnlyUnits.Ticks, TimeOnlyUnits.Hours, TimeOnlyUnits.Minutes, TimeOnlyUnits.Seconds,
TimeOnlyUnits.Milliseconds, TimeOnlyUnits.Microseconds, TimeOnlyUnits.Nanoseconds)]
@@ -81,6 +85,60 @@ public void Constructor_with_representation_should_return_expected_result(
subject.Units.Should().Be(units);
}
+ [Theory]
+ [InlineData("""{ "x" : { "Hour" : { "$numberInt" : "8" }, "Minute" : { "$numberInt" : "32" }, "Second" : { "$numberInt" : "5" }, "Millisecond" : { "$numberInt" : "594" }, "Microsecond" : { "$numberInt" : "658" }, "Nanosecond" : { "$numberInt" : "300" }, "Ticks" : { "$numberLong" : "307255946583" } } }""","08:32:05.5946583" )]
+ [InlineData("""{ "x" : { "Hour" : { "$numberInt" : "0" }, "Minute" : { "$numberInt" : "0" }, "Second" : { "$numberInt" : "0" }, "Millisecond" : { "$numberInt" : "0" }, "Microsecond" : { "$numberInt" : "0" }, "Nanosecond" : { "$numberInt" : "0" }, "Ticks" : { "$numberLong" : "0" } } }""","00:00:00.0000000" )]
+ [InlineData("""{ "x" : { "Hour" : { "$numberInt" : "23" }, "Minute" : { "$numberInt" : "59" }, "Second" : { "$numberInt" : "59" }, "Millisecond" : { "$numberInt" : "999" }, "Microsecond" : { "$numberInt" : "999" }, "Nanosecond" : { "$numberInt" : "900" }, "Ticks" : { "$numberLong" : "863999999999" } } }""","23:59:59.9999999" )]
+ public void Deserialize_with_document_should_have_expected_result(string json, string expectedResult)
+ {
+ var subject = new TimeOnlySerializer();
+ TestDeserialize(subject, json, expectedResult);
+ }
+
+ [Theory]
+ [InlineData("""{ "x" : { "Hour" : { "$numberInt" : "8" }, "Minute" : { "$numberInt" : "32" }, "Second" : { "$numberInt" : "5" }, "Millisecond" : { "$numberInt" : "594" }, "Microsecond" : { "$numberInt" : "658" }, "Ticks" : { "$numberLong" : "307255946583" } } }""","08:32:05.5946583" )]
+ [InlineData("""{ "x" : { "Hour" : { "$numberInt" : "8" }, "Minute" : { "$numberInt" : "32" }, "Second" : { "$numberInt" : "5" }, "Millisecond" : { "$numberInt" : "594" }, "Nanosecond" : { "$numberInt" : "300" }, "Ticks" : { "$numberLong" : "307255946583" } } }""","08:32:05.5946583" )]
+ [InlineData("""{ "x" : { "Hour" : { "$numberInt" : "8" }, "Minute" : { "$numberInt" : "32" }, "Second" : { "$numberInt" : "5" }, "Millisecond" : { "$numberInt" : "594" }, "Ticks" : { "$numberLong" : "307255946583" } } }""","08:32:05.5946583" )]
+ public void Deserialize_with_document_should_work_with_missing_microsecond_or_nanosecond(string json, string expectedResult)
+ {
+ var subject = new TimeOnlySerializer();
+ TestDeserialize(subject, json, expectedResult);
+ }
+
+ [Theory]
+ [InlineData("""{ "x" : { "Hour" : { "$numberInt" : "7" }, "Minute" : { "$numberInt" : "32" }, "Second" : { "$numberInt" : "5" }, "Millisecond" : { "$numberInt" : "594" }, "Microsecond" : { "$numberInt" : "658" }, "Nanosecond" : { "$numberInt" : "300" }, "Ticks" : { "$numberLong" : "307255946583" } } }""")]
+ [InlineData("""{ "x" : { "Hour" : { "$numberInt" : "8" }, "Minute" : { "$numberInt" : "33" }, "Second" : { "$numberInt" : "5" }, "Millisecond" : { "$numberInt" : "594" }, "Microsecond" : { "$numberInt" : "658" }, "Nanosecond" : { "$numberInt" : "300" }, "Ticks" : { "$numberLong" : "307255946583" } } }""")]
+ [InlineData("""{ "x" : { "Hour" : { "$numberInt" : "8" }, "Minute" : { "$numberInt" : "32" }, "Second" : { "$numberInt" : "6" }, "Millisecond" : { "$numberInt" : "594" }, "Microsecond" : { "$numberInt" : "658" }, "Nanosecond" : { "$numberInt" : "300" }, "Ticks" : { "$numberLong" : "307255946583" } } }""")]
+ public void Deserialize_with_document_should_throw_when_component_is_not_correct(string json)
+ {
+ var subject = new TimeOnlySerializer();
+
+ 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("Deserialized TimeOnly components do not match the ticks value.");
+ }
+
+ [Fact]
+ public void Deserialize_with_document_should_throw_when_field_is_unknown()
+ {
+ const string json = """{ "x" : { "Unknown": "test", Ticks: { "$numberDouble" : "307255946583" } } }""";
+ var subject = new TimeOnlySerializer();
+
+ 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 element: 'Unknown'.");
+ }
+
[Theory]
[InlineData("""{ "x" : "08:32:05.5946583" }""","08:32:05.5946583" )]
[InlineData("""{ "x" : "00:00:00.0000000" }""","00:00:00.0000000")]
@@ -273,6 +331,17 @@ public void GetHashCode_should_return_zero()
result.Should().Be(0);
}
+ [Theory]
+ [InlineData("08:32:05.5946583", """{ "x" : { "Hour" : { "$numberInt" : "8" }, "Minute" : { "$numberInt" : "32" }, "Second" : { "$numberInt" : "5" }, "Millisecond" : { "$numberInt" : "594" }, "Microsecond" : { "$numberInt" : "658" }, "Nanosecond" : { "$numberInt" : "300" }, "Ticks" : { "$numberLong" : "307255946583" } } }""")]
+ [InlineData("00:00:00.0000000", """{ "x" : { "Hour" : { "$numberInt" : "0" }, "Minute" : { "$numberInt" : "0" }, "Second" : { "$numberInt" : "0" }, "Millisecond" : { "$numberInt" : "0" }, "Microsecond" : { "$numberInt" : "0" }, "Nanosecond" : { "$numberInt" : "0" }, "Ticks" : { "$numberLong" : "0" } } }""")]
+ [InlineData("23:59:59.9999999", """{ "x" : { "Hour" : { "$numberInt" : "23" }, "Minute" : { "$numberInt" : "59" }, "Second" : { "$numberInt" : "59" }, "Millisecond" : { "$numberInt" : "999" }, "Microsecond" : { "$numberInt" : "999" }, "Nanosecond" : { "$numberInt" : "900" }, "Ticks" : { "$numberLong" : "863999999999" } } }""")]
+ public void Serialize_with_document_representation_should_have_expected_result(string valueString, string expectedResult)
+ {
+ var subject = new TimeOnlySerializer(BsonType.Document);
+
+ TestSerialize(subject, valueString, expectedResult);
+ }
+
[Theory]
[InlineData(BsonType.String, "08:32:05.5946583", """{ "x" : "08:32:05.5946583" }""")]
[InlineData(BsonType.String, "00:00:00.0000000", """{ "x" : "00:00:00.0000000" }""")]
@@ -407,8 +476,8 @@ public void Serializer_should_be_registered()
[Theory]
[ParameterAttributeData]
public void WithRepresentation_should_return_expected_result(
- [Values(BsonType.String, BsonType.Int64, BsonType.Int32, BsonType.Double)] BsonType oldRepresentation,
- [Values(BsonType.String, BsonType.Int64, BsonType.Int32, BsonType.Double)] BsonType newRepresentation)
+ [Values(BsonType.String, BsonType.Int64, BsonType.Int32, BsonType.Double, BsonType.Document)] BsonType oldRepresentation,
+ [Values(BsonType.String, BsonType.Int64, BsonType.Int32, BsonType.Double, BsonType.Document)] BsonType newRepresentation)
{
var subject = new TimeOnlySerializer(oldRepresentation);
@@ -473,6 +542,9 @@ private class TestClass
[BsonTimeOnlyOptions(BsonType.Int64, TimeOnlyUnits.Nanoseconds )]
public TimeOnly Nanoseconds { get; set; }
+
+ [BsonTimeOnlyOptions(BsonType.Document)]
+ public TimeOnly Document { get; set; }
}
}
#endif