3

I have a database with measured values from devices that I want to display in a web frontend. First I send the list of devices to the frontend together with the IANA timezone specifier for each device.

I would like all timestamps to be exchanged as UTC. The user selects a time range in the frontend in device-local time. I use moment.js to convert these timestamps to UTC with the known timezone of the device like this:

var startTimestamp = new Date(2017, 7, 1); //some local timestamp (zero-based month!)

var m = moment.tz(startTimestamp, "Europe/Berlin");

var utc = moment.utc(m).format();

utc is now "2017-07-31T22:00:00Z" which seems to be correct given the 2 hours offset for Berlin in DST.

I send this utc timestamp to my ASP.NET Core backend. The controller looks like this:

[HttpGet]
public IEnumerable<TimestampedValue> GetValues(int id, DateTime startTimestamp)
{
...
}

The problem is that startTimestamp is 2017-08-01 00:00:00 when the controller is called and its Kind property is set to Local. I would have expected it to be the same UTC timestamp.

Any idea what I'm doing wrong? I think moment.js is doing its job correctly so this must be a problem on the server side. If I recall correctly, the deserialization is done by JSON.net but I don't understand why it does not respect the Z at the end of the time string.

NicolasR
  • 2,222
  • 3
  • 23
  • 38
  • Have you checked the value of `startTimestamp` that is being sent from the browser? – Brad Aug 10 '17 at 06:58
  • Just checked it. The generated request is http:// localhost:53021/api/Values/GetValues?id=1&startTimestamp=2017-07-31T22%3A00%3A00Z – NicolasR Aug 10 '17 at 08:01
  • 2
    Related: https://stackoverflow.com/a/43592851/3744182. The comment there may be relevant: `DateTimeZoneHandling = DateTimeZoneHandling.Utc` *does work when the date is bound FromBody which is JSON. But not when the query string is bound, as in the question.* Can you try putting the `startTimestamp` in the request body? Or actually, the question itself has an embedded answer. Does that work? – dbc Aug 11 '17 at 05:39
  • You are right, using post method and json in the body it works as expected. Inspired by your comment I found this SO which has a longer explanation. It's not about asp.net core but I think it is the same problem. I will further investigate whether asp.net core allows me to override the model binding such that it also works with GET requests. If you post your comment as a question, I would accept it ;-) – NicolasR Aug 11 '17 at 18:34
  • 1
    @NicolasR - *I found this SO* - did you forget to include the link? *If you post your comment as a question, I would accept it* - I didn't come up with an answer, just a hint. It sounds like you actually have an answer, so why don't you add it yourself (or answer https://stackoverflow.com/a/43592851/3744182) so others can know the solution? – dbc Aug 11 '17 at 19:23
  • Ok, commented there and answered here, thanks. – NicolasR Aug 11 '17 at 23:43

1 Answers1

4

After @dbc pointed me to the different behavior between GET and POST requests I come to this conclusion:

Since my request uses the GET method and query strings are not JSON, there is no JSON.net involved in the problem, it is the default .NET Core DateTimeConverter that does the conversion. Moment.js correctly converts the timestamp to a UTC string, I checked that using the browser developer tools.

The code for DateTimeConverter can be found here: https://github.com/dotnet/corefx/blob/312736914d4e98c2948778bacac029aa831dd6b5/src/System.ComponentModel.TypeConverter/src/System/ComponentModel/DateTimeConverter.cs

As can be seen there, the converter uses DateTime.Parse. It can be tested in a simple test project that DateTime.Parse does not respect the Z-suffix. This is also discussed here DateTimeConverter converting from UTC string.

I think there would be at least four solutions

1) write a custom model binder. These SOs each show a part of it
Custom DateTime model binder in ASP.NET Core 1 (RTM)
https://dotnetcoretutorials.com/2016/12/28/custom-model-binders-asp-net-core/

2) write a custom type converter that overrides the default DateTime converter and checks whether there is a trailing Z. If so, use DateTime.Parse with the DateTimeStyles.AdjustToUniversal. Else fall back to the default implementation. I like this solution but I currently don't know how to replace the default DateTimeConverter.

3) replace all relevant DateTime parameters in the controllers with DateTimeOffset. DateTimeOffset seems to correctly convert the UTC string.

4) use a POST instead of a GET request with JSON in the request body. JSON.net seems to correctly convert the UTC string.

My preferred solution is currently a mixture of 3 and 4, depending on the context.

NicolasR
  • 2,222
  • 3
  • 23
  • 38