Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing UTC DateTime to Web API HttpGet Method results in local time

I'm trying to pass a UTC date as a query string parameter to a Web API method. The URL looks like

/api/order?endDate=2014-04-01T00:00:00Z&zoneId=4

The signature of the method looks like

[HttpGet]
public object Index(int zoneId, DateTime? endDate = null)

The date is coming in as 31/03/2014 8:00:00 PM but I'd like it to come in as 01/04/2014 12:00:00 AM

My JsonFormatter.SerializerSettings looks like this

new JsonSerializerSettings
{
    ContractResolver = new CamelCasePropertyNamesContractResolver(),
    DateTimeZoneHandling = DateTimeZoneHandling.Utc,
    DateFormatHandling = DateFormatHandling.IsoDateFormat
};

EDIT #1: I've noticed when I POST 2014-04-01T00:00:00Z it will serialize to the UTC DateTime kind in C#. However I've found a work around of doing endDate.Value.ToUniversalTime() to convert it although I find it odd how it works for a POST but not a GET.

like image 555
Ryan Avatar asked Mar 22 '14 17:03

Ryan


4 Answers

The query string parameter value you are sending 2014-04-01T00:00:00Z 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.

like image 173
Badri Avatar answered Oct 18 '22 02:10

Badri


If you want the conversion to be transparent, then you could use a custom TypeConverter:

public sealed class UtcDateTimeConverter : DateTimeConverter
{
    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        return ((DateTime)base.ConvertFrom(context, culture, value)).ToUniversalTime();
    }
}

and wire it up using:

TypeDescriptor.AddAttributes(typeof(DateTime), new TypeConverterAttribute(typeof(UtcDateTimeConverter)));

Then the query string parameter will be instantiated as DateTimeKind.Utc.

like image 27
Sean Fausett Avatar answered Oct 18 '22 02:10

Sean Fausett


I ended up just using the ToUniversalTime() method as parameters come in.

like image 13
Ryan Avatar answered Oct 18 '22 01:10

Ryan


So, for those of you who do not wish to override string-to-date conversion in your entire application, and also don't want to have to remember to modify every method that takes a date parameter, here's how you do it for a Web API project.

Ultimately, the general instructions come from here:

https://learn.microsoft.com/en-us/aspnet/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api#model-binders

Here's the specialized instructions for this case:

  1. In your "WebApiConfig" class, add the following:

        var provider = new SimpleModelBinderProvider(typeof(DateTime),new UtcDateTimeModelBinder());
        config.Services.Insert(typeof(ModelBinderProvider), 0, provider);
    
  2. Create a new class called UtcDateTimeModelBinder:

    public class UtcDateTimeModelBinder : IModelBinder
    {
        public bool BindModel(HttpActionContext actionContext,
            ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType != typeof(DateTime)) return false;
    
            var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (val == null)
            {
                return false;
            }
    
            var key = val.RawValue as string;
            if (key == null)
            {
                bindingContext.ModelState.AddModelError(bindingContext.ModelName,
                    "Wrong value type");
                return false;
            }
    
            DateTime result;
            if (DateTime.TryParse(key, out result))
            {
                bindingContext.Model = result.ToUniversalTime();
                return true;
            }
    
            bindingContext.ModelState.AddModelError(bindingContext.ModelName,
                "Cannot convert value to Utc DateTime");
            return false;
        }
    }
    
like image 2
Reginald Blue Avatar answered Oct 18 '22 01:10

Reginald Blue