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..cd6f0e21361 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,17 @@ public TimeOnlySerializer(BsonType representation, TimeOnlyUnits units)
_representation = representation;
_units = units;
+
+ _helper = new SerializerHelper
+ (
+ new SerializerHelper.Member("Hour", Flags.Hour, isOptional: true),
+ new SerializerHelper.Member("Minute", Flags.Minute, isOptional: true),
+ new SerializerHelper.Member("Second", Flags.Second, isOptional: true),
+ new SerializerHelper.Member("Millisecond", Flags.Millisecond, isOptional: true),
+ 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 +124,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 +156,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;
@@ -145,6 +185,7 @@ public override void Serialize(BsonSerializationContext context, BsonSerializati
bsonWriter.WriteString(value.ToString("o"));
break;
+
default:
throw new BsonSerializationException($"'{_representation}' is not a valid TimeOnly representation.");
}
@@ -196,6 +237,29 @@ private TimeOnly FromInt64(long value, TimeOnlyUnits units)
: new TimeOnly(value * TicksPerUnit(units));
}
+ private TimeOnly FromDocument(BsonDeserializationContext context)
+ {
+ var bsonReader = context.Reader;
+ var ticks = 0L;
+
+ _helper.DeserializeMembers(context, (_, flag) =>
+ {
+ switch (flag)
+ {
+ case Flags.Hour:
+ case Flags.Minute:
+ case Flags.Second:
+ case Flags.Millisecond:
+ case Flags.Microsecond:
+ case Flags.Nanosecond:
+ bsonReader.SkipValue(); break; // ignore value (use Ticks instead)
+ case Flags.Ticks: ticks = Int64Serializer.Instance.Deserialize(context); break;
+ }
+ });
+
+ return FromInt64(ticks, TimeOnlyUnits.Ticks);
+ }
+
private long TicksPerUnit(TimeOnlyUnits units)
{
return units switch
@@ -231,6 +295,18 @@ private long ToInt64(TimeOnly timeOnly, TimeOnlyUnits units)
: timeOnly.Ticks / TicksPerUnit(units);
}
+ private int GetNanosecondsComponent(long ticks)
+ {
+ // ticks % 10 * 100
+ return (int)(ticks % TicksPerUnit(TimeOnlyUnits.Microseconds) * 100);
+ }
+
+ private int GetMicrosecondsComponent(long ticks)
+ {
+ // ticks / 10 % 1000
+ return (int)(ticks / TicksPerUnit(TimeOnlyUnits.Microseconds) % 1000);
+ }
+
// explicit interface implementations
IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation)
{
diff --git a/tests/MongoDB.Bson.Tests/Serialization/Serializers/TimeOnlySerializerTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Serializers/TimeOnlySerializerTests.cs
index ea344347f56..54d781a27bd 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,53 @@ public void Constructor_with_representation_should_return_expected_result(
subject.Units.Should().Be(units);
}
+ [Theory]
+ [InlineData("""{ "x" : { Ticks: { "$numberLong" : "307255946583" } } }""","08:32:05.5946583" )]
+ [InlineData("""{ "x" : { Ticks: { "$numberLong" : "0" } } }""","00:00:00.0000000" )]
+ [InlineData("""{ "x" : { 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" : { Ticks: { "$numberDouble" : "307255946583" } } }""","08:32:05.5946583" )]
+ [InlineData("""{ "x" : { Ticks: { "$numberDecimal" : "307255946583" } } }""","08:32:05.5946583" )]
+ public void Deserialize_with_document_should_be_forgiving_of_actual_numeric_type(string json, string expectedResult)
+ {
+ var subject = new TimeOnlySerializer();
+ TestDeserialize(subject, json, expectedResult);
+ }
+
+ [Theory]
+ [InlineData("""
+ { "x" : { Hour: { "$numberInt": 0 }, Minute: { "$numberInt": 0 }, Second: { "$numberInt": 0 },
+ Millisecond: { "$numberInt": 0 }, Microsecond: { "$numberInt": 0 }, Nanosecond: { "$numberInt": 0 },
+ Ticks: { "$numberDouble" : "307255946583" } } }
+ ""","08:32:05.5946583" )]
+ public void Deserialize_with_document_should_ignore_other_time_components(string json, string expectedResult)
+ {
+ var subject = new TimeOnlySerializer();
+ TestDeserialize(subject, json, expectedResult);
+ }
+
+ [Theory]
+ [InlineData("""{ "x" : { "Unknown": "test", Ticks: { "$numberDouble" : "307255946583" } } }""" )]
+ public void Deserialize_with_document_should_throw_when_field_is_unknown(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("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 +324,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 +469,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 +535,9 @@ private class TestClass
[BsonTimeOnlyOptions(BsonType.Int64, TimeOnlyUnits.Nanoseconds )]
public TimeOnly Nanoseconds { get; set; }
+
+ [BsonTimeOnlyOptions(BsonType.Document)]
+ public TimeOnly Document { get; set; }
}
}
#endif