20

So I have a WebAPI 2 controller written in C# that takes among other things a query parameter of type DateTime. This is an API that returns all values from the data store based on a date filter. Something like, let's say:

public MyThing GetThing([FromUri]DateTime startTime)
{
 // filter and return some results
}

I am running into 2 problems:

  1. For some reason despite passing in a ISO 8601 UTC formatted (with a Z) date, WebAPI is de-serializing it as a local DateTime, instead of Utc. This is obviously undesirable. I am not sure how to modify the pipeline to have it correctly understand UTC-0 DateTimes.
  2. I am returning a link back to the resource as part of the response body, in which I use the UrlHelper objects (obtained from the parent ApiController abstract class) Link() method to generate an href. I am passing a collection of query parameters I want added to the route. For whatever reason passing the DateTime formats it in a non-ISO8601 format. I can't find where this is controlled. I don't want to explicitly ToString() it as that's not enforceable universally.

In short, I want to figure out how to make sure that

  1. DateTimes that are passed in via FromUri query params are properly understood as ISO8601, including appropriate time zone offsets
  2. UrlHelper.Link() generates ISO8601-compliant DateTimes in the output URI string in a universally enforceable statically-typed way.

WebAPI 2 does provide wonderful hooks for formatting JSON, which I do make use of, so simply returning a DateTime in a JSON body formats it as desired using the ISO8601 format, and as well it is correctly understood in a [FromBody] JSON body. I can't find ways for pulling strings around URI handling though, and I would really like to!

Nkosi
  • 235,767
  • 35
  • 427
  • 472
Zoinks
  • 361
  • 1
  • 2
  • 11
  • 2
    About UrlHelper, it seems like it's kinda deprecated, not sure about that. I believe it ends in the (internal) Bind method here: http://sourcebrowser.io/Browse/ASP-NET-MVC/aspnetwebstack/src/System.Web.Http/Routing/HttpParsedRoute.cs as you see it just does a `Convert.ToString(value, CultureInfo.InvariantCulture)` on parameters. So, the only solution would be to add a DateTime wrapper class that overrides the ToString() to send back what you want. – Simon Mourier Jun 21 '18 at 05:19
  • 3
    As for the FromUri, I suggest you use [ValueProvider(typeof(MyUriValueProviderFactory))] instead of [FromUri] and implement MyUriValueProviderFactory. It must derive from ValueProviderFactory. – Simon Mourier Jun 21 '18 at 06:17
  • 1
    this might help: https://stackoverflow.com/questions/22581138/passing-utc-datetime-to-web-api-httpget-method-results-in-local-time – Andrei Dragotoniu Jun 21 '18 at 09:49
  • 1
    Dates are always a massive pain. 9 times out of 10 it's easier to pass it as string, and then use whatever conversion you need in your own code. – ste-fu Jun 21 '18 at 10:56
  • 1
    Are You using Route attributes? In past I had problems with dates, but for example for method `public async Task GetDetails(DateTime? start = null, DateTime? end = null)` I'm using this route `[Route("details/{start:datetime:regex(\\d{4}-\\d{2}-\\d{2})?}/{end:datetime:regex(\\d{4}-\\d{2}-\\d{2})?}")]` – Misiu Jun 26 '18 at 06:31

4 Answers4

9

You can use modelbinder to transforming incoming data to your model.

GetThings([ModelBinder(typeof(UtcDateTimeModelBinder)), FromUri] DateTime dt){//do somthing}


public class UtcDateTimeModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {

        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        if (bindingContext.ModelMetadata.ModelType == typeof(DateTime))
        {
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            var str = valueProviderResult.AttemptedValue;
            return DateTime.Parse(str).ToUniversalTime();
        }

        return null;
    }

In that way you can set it as default modelbilder of DateTime.

ModelBinders.Binders.Add(typeof(DateTime), new UtcDateTimeModelBinder());
reza taroosheh
  • 196
  • 2
  • 5
6

Why not using DateTimeOffset instead of DateTime if you want to keep UTC offset? Here are some code working with JSON serialization:

Api Controller:

public class ValuesController : ApiController
{
    public object Get(DateTimeOffset dt)
    {
        return new {
            Input = dt,
            Local = dt.LocalDateTime,
            Utc = dt.UtcDateTime
        };
    }
}

Razor View sample code (assuming you have the default api route created in a MVC + Web API Visual studio template )

<a href="@Url.RouteUrl("DefaultApi",new {httproute = "",controller = "values",dt = DateTimeOffset.UtcNow})">Utc Now</a>

Rendered as:

<a href="/api/values?dt=06%2F26%2F2018%2009%3A37%3A24%20%2B00%3A00">Utc Now</a>

And you can call your API with datetime offset:

2018-06-26T08:25:48Z: http://localhost:1353/api/values?dt=2018-06-26T08:25:48Z
{"Input":"2018-06-26T08:25:48+00:00","Local":"2018-06-26T10:25:48+02:00","Utc":"2018-06-26T08:25:48Z"}

2018-06-26T08:25:48+01:00: http://localhost:1353/api/values?dt=2018-06-26T08%3A25%3A48%2B01%3A00 (note that : and + must be url encoded)
{"Input":"2018-06-26T08:25:48+01:00","Local":"2018-06-26T09:25:48+02:00","Utc":"2018-06-26T07:25:48Z"}
asidis
  • 1,374
  • 14
  • 24
2

The query string parameter value you are sending is UTC time. So, the same gets translated to a time based on your local clock and if you call ToUniversalTime(), it gets converted back to UTC.

So, what exactly is the question? If the question is why is this happening if sent in as query string but not when posted in request body, the answer to that question is that ASP.NET Web API binds the URI path, query string, etc using model binding and the body using parameter binding. For latter, it uses a media formatter. If you send JSON, the JSON media formatter is used and it is based on JSON.NET.

Since you have specified DateTimeZoneHandling.Utc, it uses that setting and you get the date time kind you want. BTW, if you change this setting to DateTimeZoneHandling.Local, then you will see the same behavior as model binding.

Meaning in order to get the desired formatting you want, all you need to do is call the ToUniversalTime() method.

Barr J
  • 10,636
  • 1
  • 28
  • 46
  • 2
    The op wants this to be model bound, and has even put a bounty on this, There could be other why this should be done, such as this is in an existing project and the user does not want to to manually call that every where. Please refactor your answer – johnny 5 Jun 25 '18 at 01:33
1

1.

You should check the timezone of your parameter 'startTime' (which should be the timezone of your server/computer).

The DateTime provided by Web API is correct, it just depends on YOUR timezone.

2.

Create a Json DateTime serializer in order to generate ISO8601 formatted date.

sheep
  • 31
  • 3