31

I have this url

http://example.com/api/record/getall?startdate=1994-11-05T17:15:30Z

and this webapi endpoint

[ActionName("GetAll")]
public object GetAll(DateTime startDate)
{
     ...
}

The problem I face is that the startDate received the deserialized string as a local time, "11/5/1994 9:15:30 AM" instead of stay in UTC time which what I wanted "11/5/1994 5:15:30 PM".

I'm using VS2012 update2, latest Json.net nuget package. However, if I use json.net in a separate console app to test, the same string "1994-11-05T17:15:30Z" is able to deserialize correctly into "11/5/1994 5:15:30 PM".

Anyone know what is wrong here?

Dale K
  • 25,246
  • 15
  • 42
  • 71
Ray
  • 12,101
  • 27
  • 95
  • 137

3 Answers3

32

Although you have already found a solution for your question, I thought I would take a shot at explaining why it did not work as you expected.

WebApi uses content type negotiation to determine what parser to use when reading data. That means it will look at the Content-Type header of the request to make the determination. If the Content-Type header is set to application/json then it will use Json.Net to parse to content and feed it to your method.

An HTTP GET request, such as the one you are making here, does not have a content type set. The "content" in this case is really just the query string from the URL. WebApi does not expect to find JSON data here, so it is not going to try to use a JSON parser to make sense of it. Even if it did, the string you are passing to your GetAll method isn't even valid JSON. (It would need to be quoted to be valid.)

Now, if you were to change your method to accept a POST request, and you set the content type header to application/json and passed the date as a JSON string in the body, then WebApi will use Json.Net to parse it, and it will work like you expect.

For example, say your method looked like this:

[HttpPost]
public object GetAll([FromBody]DateTime startDate)
{
    try
    {
        return new
        {
            StartDate = startDate.ToString("yyyy-MM-dd HH:mm:ss"),
            StartDateKind = startDate.Kind.ToString(),
        };
    }
    catch (Exception ex)
    {
        return ex.Message;
    }
}

And you made a request like this (note the POST):

POST http://localhost:57524/api/values/GetAll HTTP/1.1
Content-Type: application/json
Content-Length: 22
Host: localhost:57524

"1994-11-05T17:15:30Z"

The response would look like this:

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Fri, 31 May 2013 01:25:48 GMT
Content-Length: 57

{"StartDate":"1994-11-05 17:15:30","StartDateKind":"Utc"}

As you can see, it does correctly recognize the date to be UTC in this scenario.

Community
  • 1
  • 1
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • I really appreciate your taking the time to explain. +1 and marked you as answer. – Ray Jun 12 '13 at 22:14
  • Unfortunately this does not work with GET request with [FromUri] attribute attached to your request param. Not sure how to get it working. – Amitava Jun 21 '13 at 07:06
  • I know this is a bit old post but did anyone found a solution for [FromUri] attribute when parsing dates ? – khorvat Jun 12 '14 at 11:47
  • 4
    Yeah although this explains why this is happening I'd like to know how to solve this without resorting to manual date conversion inside the controllers. – Harindaka Jul 24 '14 at 10:28
6

If you want to modify the way Asp WebApi parses uri parameters of your GET requests, you can write custom IModelBinder like this:

public class UtcDateTimeModelBinder : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        var stringValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName)?.RawValue as string;
        DateTime parsedDate;
        if (!DateTime.TryParse(stringValue, null, DateTimeStyles.AdjustToUniversal, out parsedDate))
        {
            return false;
        }

        bindingContext.Model = parsedDate;
        return true;
    }
}

And then override default binder:

GlobalConfiguration.Configure(configuration => configuration.BindParameter(typeof(DateTime), new UtcDateTimeModelBinder()););

Parameter Binding in ASP.NET Web API

Atomosk
  • 1,871
  • 2
  • 22
  • 26
  • If your date is already formatted in ISO format "1994-11-05T17:15:30Z". then the DateTime.TryParse should be modified as DateTime.TryParse(stringValue, null, DateTimeStyles.RoundtripKind, out parsedDate) – Dilhan Jayathilake Oct 10 '16 at 04:37
0

Just use DateTimeOffset instead of DateTime as the input type for your API:

[ActionName("GetAll")]
public object GetAll(DateTimeOffset startDate)
{
     ...
}

Pass the date in the querystring as UTC e.g.

startDate=2021-10-01T12:00:00.000Z

This will create a C# DateTimeOffset and itsUtcDateTime property will have the UTC DateTime.

(The LocalDateTime property of the DateTimeOffset will also be the UTC DateTime, and the Offset property of the DateTimeOffset will be a TimeSpan of 00:00:00. This is different to a DateTime, which instead has its Kind property set to Utc for a UTC DateTime)

Using DateTimeOffset also has the advantage of allowing the date to be passed as a local time e.g. the following date can be passed to the API:

startDate=2021-10-01T18:00:00.000+05:00

Note that when passing this in the querystring, the + must be URL encoded as %2B:

startDate=2021-10-01T18:00:00.000%2B05:00

This will convert to a DateTimeOffset representing the local time in a timezone 5 hours ahead of UTC:

startDate.DateTime = {1/10/21 6:00:00 pm}
startDate.UtcDateTime = {1/10/21 1:00:00 pm}
startDate.LocalDateTime = {1/10/21 1:00:00 pm}
startDate.Offset = {05:00:00}
Chris Halcrow
  • 28,994
  • 18
  • 176
  • 206