Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public BsonTimeOnlyOptionsAttribute(BsonType representation)
/// Initializes a new instance of the BsonTimeOnlyOptionsAttribute class.
/// </summary>
/// <param name="representation">The external representation.</param>
/// <param name="units">The TimeOnlyUnits.</param>
/// <param name="units">The TimeOnlyUnits. Ignored if representation is BsonType.Document.</param>
public BsonTimeOnlyOptionsAttribute(BsonType representation, TimeOnlyUnits units)
{
_representation = representation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
121 changes: 117 additions & 4 deletions src/MongoDB.Bson/Serialization/Serializers/TimeOnlySerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/

using System;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization.Options;

namespace MongoDB.Bson.Serialization.Serializers
Expand All @@ -32,7 +33,20 @@ public sealed class TimeOnlySerializer: StructSerializerBase<TimeOnly>, IReprese
/// </summary>
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;

Expand All @@ -58,11 +72,12 @@ public TimeOnlySerializer(BsonType representation)
/// Initializes a new instance of the <see cref="TimeOnlySerializer"/> class.
/// </summary>
/// <param name="representation">The representation.</param>
/// <param name="units">The units.</param>
/// <param name="units">The units. Ignored if representation is BsonType.Document.</param>
public TimeOnlySerializer(BsonType representation, TimeOnlyUnits units)
{
switch (representation)
{
case BsonType.Document:
case BsonType.Double:
case BsonType.Int32:
case BsonType.Int64:
Expand All @@ -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
Expand All @@ -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)
};
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -175,6 +218,61 @@ public TimeOnlySerializer WithRepresentation(BsonType representation, TimeOnlyUn
}

// private methods

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to add a blank line here?

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
Expand All @@ -196,6 +294,20 @@ private TimeOnly FromInt64(long value, TimeOnlyUnits units)
: new TimeOnly(value * TicksPerUnit(units));
}

private int GetNanosecondsComponent(long ticks)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put in alphabetical order?

{
// ticks * 100 % 1000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment isn't very helpful. That's exactly what the code says.

var nanosecondsPerTick = 100;
return (int)(ticks * nanosecondsPerTick % 1000);
}

private int GetMicrosecondsComponent(long ticks)
{
// ticks / 10 % 1000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment isn't very helpful. That's exactly what the code says.

var ticksPerMicrosecond = TicksPerUnit(TimeOnlyUnits.Microseconds);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine to the TicksPerUnit method if you want. My thought was to eliminate a method call for what was essentially a constant.

return (int)(ticks / ticksPerMicrosecond % 1000);
}

private long TicksPerUnit(TimeOnlyUnits units)
{
return units switch
Expand Down Expand Up @@ -231,6 +343,7 @@ private long ToInt64(TimeOnly timeOnly, TimeOnlyUnits units)
: timeOnly.Ticks / TicksPerUnit(units);
}


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to add a blank line here?

// explicit interface implementations
IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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)]
Expand All @@ -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<BsonSerializationException>();
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<BsonSerializationException>();
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")]
Expand Down Expand Up @@ -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" }""")]
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down