CsvHelper icon indicating copy to clipboard operation
CsvHelper copied to clipboard

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

Open tdjastrzebski opened this issue 2 years ago • 6 comments

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.

tdjastrzebski avatar Dec 01 '23 15:12 tdjastrzebski

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());

JoshClose avatar Dec 01 '23 21:12 JoshClose

Thank you, @JoshClose

tdjastrzebski avatar Dec 02 '23 10:12 tdjastrzebski

Does this solve your problem?

JoshClose avatar Dec 04 '23 16:12 JoshClose

@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 avatar Dec 05 '23 15:12 tdjastrzebski

@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.

tdjastrzebski avatar Jan 05 '24 10:01 tdjastrzebski

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.

JoshClose avatar Jan 05 '24 15:01 JoshClose