JoshClose / CsvHelper

Library to help reading and writing CSV files
http://joshclose.github.io/CsvHelper/
Other
4.65k stars 1.05k forks source link

Globally turn off date/time conversion to local, always use UTC #2211

Open tdjastrzebski opened 7 months ago

tdjastrzebski commented 7 months ago

The problem: in server-side running code (e.g. Azure Function) I always want date/time strings to be deserialized as UTC DateTime. In my test environment string like "2023-10-26T14:50:11.365+00:00" is deserialized with time part value 16:50:11 (local). I know the TypeConverterOption for individual fields can be defined like this Map( m => m.DateTimeProperty ).TypeConverterOption( DateTimeStyles.AssumeUniversal ); or class property attribute can be added, but I would like to turn time conversion off globally, preferably using CsvConfiguration and just forget about it since in server-side software I never want conversion to local time. The necessity to implement such conversion everywhere is VERY error-prone as I recently learned. For the same reason I would like to avoid any post-fixing e.g. by calling ToUniversalTime(). After deserialization, in server-side/cloud-running code I ALWAYS want DateTime to be UTC, not local.

I am not suggesting change of the default behavior, but adding some option to change it - unless it is already available, but I could not find any.

JoshClose commented 7 months ago

Easiest way is to use DateTimeOffset instead.

Another option would be to use a custom type converter.

public class UtcDateTimeConverter : DateTimeConverter
{
    public override object ConvertFromString(string text, IReaderRow row, MemberMapData memberMapData)
    {
        var o = base.ConvertFromString(text, row, memberMapData);

        return (o as DateTime?)?.ToUniversalTime() ?? o;
    }
}

// Register the converter.
csv.Context.TypeConverterCache.AddConverter<DateTime>(new UtcDateTimeConverter());
tdjastrzebski commented 7 months ago

Thank you, @JoshClose

JoshClose commented 7 months ago

Does this solve your problem?

tdjastrzebski commented 7 months ago

@JoshClose Yes, it solves the problem. One just needs to be careful to call AddConverter<DateTime>(..) before RegisterClassMap<MyMap<T>>() is called. This is one of the reasons why I still believe some CsvConfiguration option would be a better solution, but my problem is solved.

tdjastrzebski commented 6 months ago

@JoshClose, just one question: I realized that I do not know why your UtcDateTimeConverter seems to work. ToUniversalTime() method does not just change DateTimeKind to Utc, it also applies the required time offset and that is exactly what I try to avoid.

JoshClose commented 6 months ago

Looking at this code

var date = DateTime.Parse("2023-10-26T14:50:11.365+00:00", CultureInfo.InvariantCulture);
var dateU = date.ToUniversalTime();
date.Kind.Dump();
dateU.Kind.Dump();
date.Dump();
dateU.Dump();

with output

Local
Utc
10/26/2023 9:50:11 AM
10/26/2023 2:50:11 PM

It seems that since the time zone is included in the date string, it's smart enough to figure out creating it as local actually converts to 9:50 AM. Then you change to universal and get 2:40 PM, which is what the string shows. If you don't have the +00:00 as part of the date string, it comes out as Unspecified and the universal conversion doesn't work properly. In this case, you would want to use DateTimeStyles.AssumLocal or DateTimeStyles.AssumeUniversal.