[CSHARP-3717] Add DateOnly/TimeOnly support Created: 24/Jun/21  Updated: 11/Jan/24

Status: Backlog
Project: C# Driver
Component/s: BSON
Affects Version/s: None
Fix Version/s: None

Type: New Feature Priority: Major - P3
Reporter: Manuel Gysin Assignee: Unassigned
Resolution: Unresolved Votes: 7
Labels: net6
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified

Attachments: PNG File image-2024-01-10-14-39-54-976.png    
Issue Links:
Duplicate
is duplicated by CSHARP-4571 DateOnly serialization not working pr... Closed
Epic Link: Implement 3.0 release
Quarter: FY24Q4
Documentation Changes Summary:

1. What would you like to communicate to the user about this feature?
2. Would you like the user to see examples of the syntax and/or executable code and its output?
3. Which versions of the driver/connector does this apply to?


 Description   

Add support the data type DateOnly in the upcoming .net 6 release.

Date, Time, and Time Zone Enhancements in .NET 6

DateOnly and TimeOnly should be added to DefaultFrameworkAllowedTypes.AllowedTypes.



 Comments   
Comment by Justin Leaming [ 11/Jan/24 ]

james.kovacs@mongodb.com  thank you for the quick reply. I believe the solution to [BsonDateTimeOptions(DateOnly = true)] is used for a DateTime and our code is using a DateOnly.

In the meantime our resolution for this was to circumvent the forced UTC in BsonUtils.ToDateTimeFromMillisecondsSinceEpoch and process milliseconds ourselves.

Here is what we did for anyone else facing this issue

public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DateOnly value)
{
    var dateTime = value.ToDateTime(zeroTimeComponent);
    var ticks = (long) (dateTime - epoch).TotalMilliseconds; 
    context.Writer.WriteDateTime(ticks);
} 

Comment by James Kovacs [ 10/Jan/24 ]

DateTime in .NET defaults to DateTimeKind.Local. The driver maps DateTime to BsonDateTime in the database, which is always stored in UTC. Since the DateTime is local, the driver converts it to UTC before storing it in the database and performs the opposite conversion of UTC to local when deserializing.

To avoid this automatic conversion between local and UTC, you can always work with dates as UTC using DateTime.UtcNow and similar. Alternatively you can add the [BsonDateTimeOptions(DateOnly = true)] attribute to your DateTime properties and ensure that DateTime.TimeOfDay is TimeSpan.Zero. The driver will then ignore the DateTimeKind and store the dates as a UTC date with a time component of 12:00:00AM. For example storing DateTime.Today (which is 2024-01-10 12:00:00AM (Local)) is stored in the database as 2024-01-10 12:00:00AM (UTC).

Hope that helps in your application until the driver supports DateOnly natively.

Sincerely,
James

Comment by Justin Leaming [ 10/Jan/24 ]

james.kovacs@mongodb.com

I believe the problem comes down to this

The serialization to adds the systems time of day, even though the datetime is set to midnight. This means when querying for data by a date only value, if your system is at a different time zone than when the data was added the query will fail. The problem when we encountered this is when our time zone changed from UTC-6 to UTC-7, suddenly our data queries stopped working.[

Comment by James Kovacs [ 21/Dec/23 ]

Hi, hakon.ingvaldsen@sonat.no,

Thank you for your feedback. Our team is working on a new major release that will introduce support for newer TFMs allowing us to support DateOnly, TimeOnly, and other more recent data types.

Regarding your comment about these newer types not working with LINQ, please elaborate. I registered the DateOnlySerializer that I demonstrated in my earlier comment and it was serialized correctly by LINQ to a BsonDateTime when querying. If you can provide the failing code snippet, we can determine why it is not working as expected in your use case.

Sincerely,
James

Comment by Håkon Ingvaldsen [ 21/Dec/23 ]

Its been a few years now, DateOnly and TimeOnly is still not supported as far as I can tell. We are on .NET 8 now.
I have created Custom Serializer as described here, and it works for pure storage.
But we cannot use DateOnly in LINQ for instance, in filters or elsewhere, until you holistically support the types.

Comment by Ahmed Alejo [ 13/Feb/23 ]

Hi, after all the searches I did keep pointing to the dead end that this issue represents.

And since I eventually found a fix/workaround/way-forward,

below I share with the world. 

public class DocumentDbDateOnlySerializer : StructSerializerBase<DateOnly>
{     
    IBsonSerializer<DateOnly> InnerSerializer = DateTimeSerializer.DateOnlyInstance;
        
    public override DateOnly Deserialize(
        BsonDeserializationContext context,
        BsonDeserializationArgs args)
    {
        var dateTime = InnerSerializer.Deserialize(context, args);
        return DateOnly.FromDateTime(dateTime);
    }     
    
    public override void Serialize(
        BsonSerializationContext context, 
        BsonSerializationArgs args,
        DateOnly value) =>
        InnerSerializer.Serialize(
            context, 
            args, 
            value.ToDateTime(TimeOnly.MinValue));
}

public sealed class TimeOnlyBsonSerializer : StructSerializerBase<TimeOnly>
{
    private IBsonSerializer<TimeSpan> InnerSerializer = new TimeSpanSerializer();
 
    public override TimeOnly Deserialize(
        BsonDeserializationContext context,
        BsonDeserializationArgs args)
    {
        var timespan = InnerSerializer.Deserialize(context, args);
        return TimeOnly.FromTimeSpan(timespan);
    }
 
    public override void Serialize(
        BsonSerializationContext context, 
        BsonSerializationArgs args, 
        TimeOnly value) =>
        InnerSerializer.Serialize(context, args, value.ToTimeSpan());
}

My actual scenario was a bit more involved

record NamedPrimitiveValue(
    string Name,
    object Value)

 

I can add more code if anyone is interested in how I solved it.

Hopefully, this will save someone sometime

 

Comment by James Turner [ 08/Mar/22 ]

While I know that code snippet for `DateOnlySerializer` is an example, there are a couple of issues trying to run it. One issue is that you can't use `BsonSerializer.RegisterSerializer(new DateOnlySerializer());` for the serializer as there is a serializer already (though incorrectly) registered via `BsonClassMapSerializer` in v2.14.1. You'll need to register a serialization provider and use that to return the correct serializer.

Also `ToDateTimeFromMillisecondsSinceEpoch` seems to cause issues relating to time zones. Serializing a `DateOnly` for `2020-04-08` is fine but will deserialize to `2020-04-07`.

Just mentioning this here in case anyone else hits issues with that example!

Comment by James Kovacs [ 17/Jan/22 ]

At the moment we do not explicitly target .NET 6 and therefore cannot use .NET 6-specific types like DateOnly. The driver is however compatible with .NET 6 and you can use .NET 6-specific types in your own applications with the driver. Eventually when we implement .NET 6 as a TFM then we will be able to support DateOnly directly in the driver.

In the meantime, it is trivial to implement your own DateOnly serialization support by implementing a custom IBsonSerializer. The base class StructSerializerBase<T> will handle much of the work for you. The following minimal DateOnlySerializer will store DateOnly as a BsonDateTime in the database. You could modify it to store DateOnly as a string, seconds since epoch, array of date components, etc. depending on your needs.

using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
 
// You need to register the custom serializer once during bootstrapping.
// Typically you'll do this right before creating your MongoClient singleton.
BsonSerializer.RegisterSerializer(new DateOnlySerializer());
 
internal class DateOnlySerializer : StructSerializerBase<DateOnly>
{
    private static readonly TimeOnly zeroTimeComponent = new();
 
    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DateOnly value)
    {
        var dateTime = value.ToDateTime(zeroTimeComponent);
        var ticks = BsonUtils.ToMillisecondsSinceEpoch(dateTime);
        context.Writer.WriteDateTime(ticks);
    }
 
    public override DateOnly Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        var ticks = context.Reader.ReadDateTime();
        var dateTime = BsonUtils.ToDateTimeFromMillisecondsSinceEpoch(ticks);
        return new DateOnly(dateTime.Year, dateTime.Month, dateTime.Day);
    }
}

Comment by Eugenio Blabla [ 16/Jan/22 ]

Here I'm wondering too with @Henry, it's 2022

Comment by Henry Yu [ 26/Dec/21 ]

.NET 6 has been released for more than 1 month now. Any updates?

Comment by Dmitry Lukyanov (Inactive) [ 28/Jun/21 ]

Hello manuel.gysin@gmail.com , this is a good feature request, we will consider it when the .net6 will be released

Comment by Dmitry Lukyanov (Inactive) [ 24/Jun/21 ]

Hello manuel.gysin@gmail.com , thanks for your report, we will discuss this option and come back to you soon

Generated at Wed Feb 07 21:46:06 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.