54

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.

Johannes Rudolph
  • 35,298
  • 14
  • 114
  • 172
Ryan
  • 4,354
  • 2
  • 42
  • 78

7 Answers7

42

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.

Richard
  • 4,740
  • 4
  • 32
  • 39
  • 1
    So I guess the correct thing to do is leave it the way it is and continue to convert the querystring dates back to UTC with the work around I have? What's the best approach? – Ryan Mar 22 '14 at 19:19
  • 1
    This should be the answer, it's a very clear description of exactly what is happening. – Richard Feb 03 '15 at 07:36
  • 13
    This should not be the answer. Although it explains what is going on it doesn't provide a solution to the question. The OP stated that they want the time to come in as UTC not for it to come in as local and convert to UTC. If this is not possible your answer does not make that clear, if it is you don't say how this can be achieved. I have the same question. I don't want to be calling ToUniversalTime() every time I receive a date. I would like to just receive the UTC time. Is there some deserialization setting somewhere that will do this automatically? – CSCoder Sep 07 '18 at 21:17
27

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.

Sean Fausett
  • 3,710
  • 1
  • 23
  • 17
  • See http://blogs.msdn.com/b/jmstall/archive/2012/04/20/how-to-bind-to-custom-objects-in-action-signatures-in-mvc-webapi.aspx for more information. – Sean Fausett Jul 22 '14 at 23:40
  • 2
    is there any solution to add this UtcDateTimeConverter to Register method of startup.cs class in web api ? I couldn't raise up your class – MRP Nov 25 '17 at 12:12
  • I added this to the lambda GlobalConfiguration.Configure(config => TypeDescription.AddAttributes(..... and it worked beautifully! Thank you Sean! – Brain2000 Apr 14 '22 at 18:54
13

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

Ryan
  • 4,354
  • 2
  • 42
  • 78
  • 1
    I had the same problem and none of the answers was my real answer and I ended up with your answer or add custom model binder and add [ModelBinder(typeof(UtcDateTimeModelBinder))] to every parameter you want – MRP Nov 25 '17 at 12:10
2

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;
        }
    }
    
Reginald Blue
  • 930
  • 11
  • 26
0

I finally find this code , it's not the main answer but it can be used in some cases :

var dateUtc = TimeZoneInfo.ConvertTimeToUtc(date);
mhKarami
  • 844
  • 11
  • 16
0

DateTimeOffset
Our versioned API classes are automapped to internal classes. Using DateTimeOffset in the URL parameter model of the API and adding a mapping DateTimeOffset => DateTime is effective at preventing the timezone conversion. I.E.

API Class:

public DateTimeOffset? SomeDateTime{ get; set; }

Internal Class:

public DateTime? SomeDateTime{ get; set; }

Mapping profile:

CreateMap<DateTimeOffset, DateTime>();
Peter L
  • 2,921
  • 1
  • 29
  • 31
  • This does not work when dealing with `DateTime` in the Controller. See [example code](https://github.com/p33t/learn-asp31/blob/master/api/Controller1.cs) – Peter L Jan 12 '22 at 23:33
0

[This answer expands on the answer from @SeanFausett]

I wanted to have an ISO 8601 date that could have a "Z" on the and the web api function would receive it as a Utc Kind DateTime. But if there was not a "Z", I did not want the conversion.

I also needed to convert dates from incoming POST JSON payloads. The function below can support converting a string to a DateTime, DateTime?, DateTimeOffset, or DateTimeOffset?

It's handy to have dates parse the same way whether form a JSON post or URL parameter. Feel free to tailor the conversion to suit your needs.

//Register the two converters
var jSettings = new Newtonsoft.Json.JsonSerializerSettings()
jSettings.Converters.Add(new UtcDateTimeConverterJSON());
GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings = jSettings;

GlobalConfiguration.Configure(config =>
{
    TypeDescriptor.AddAttributes(typeof(DateTime), new TypeConverterAttribute(typeof(UtcDateTimeConverterURI)));
    WebApiConfig.Register(config);
}

//Date converter for URI parameters
public class UtcDateTimeConverterURI : DateTimeConverter
{
    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
    {
        if (value?.GetType() == typeof(string))
        {
            return StringToDate(typeof(DateTime), (string)value, Path: "URI parameter");
        }
        else
        {
            return base.ConvertFrom(context, culture, value);
        }
    }

    /// <summary>
    /// Convert String to DateTime, DateTime?, DateTimeOffset, or DateTimeOffset?<br />
    /// Used for incoming JSON objects and URI parameters
    /// </summary>
    /// <param name="targetType">The type (i.e. typeof(DateTime))</param>
    /// <param name="sDate">string representation of date to be converted</param>
    /// <param name="Path">JSON Path in case of error, so the caller knows which parameter to fix</param>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    public static object StringToDate(Type targetType, string sDate, string Path)
    {
        //if the object is a DateTime, determine if we need to return a UTC or Local date type
        bool returnUTC = false;

        //DateTime or DateTimeOffset return type
        bool isDateTimeOffset;
        if (targetType == typeof(DateTime?) || targetType == typeof(DateTime))
        {
            isDateTimeOffset = false;
        }
        else
        {
            isDateTimeOffset = true;
        }

        DateTimeOffset d;

        if (String.IsNullOrEmpty(sDate))
        {
            //if we have an empty string and the type is a nullable date, then return null... otherwise throw an error
            if (targetType == typeof(DateTime?))
            {
                return null;
            }
            else
            {
                throw new Exception(Path + " cannot be an empty Date");
            }
        }

        if (sDate[0] == '/')
        {
            // /Date(xxxxx)/ format
            sDate = sDate.Substring(6, sDate.Length - 8);
            var index = sDate.LastIndexOf('-');
            if (index == -1) index = sDate.LastIndexOf('+');
            if (index >= 0)
            {
                //lop off timezone offset
                sDate = sDate.Substring(0, index);
            }
            else
            {
                //no timezone offset, return as UTC
                returnUTC = true;
            }

            if (!Int64.TryParse(sDate, out var l))
            {
                //can't parse....
                throw new Exception(Path + " cannot be parsed as a Date");
            }
            else
            {
                d = DateTimeOffset.FromUnixTimeMilliseconds(l);
            }
        }
        else
        {
            //try and parse ISO8601 string
            if (!DateTimeOffset.TryParse(sDate, out d))
            {
                throw new Exception(Path + " cannot be parsed as a Date");
            }
            else
            {
                if (!isDateTimeOffset)
                {
                    //if UTC is specifically requested and we're not returning a DateTimeOffset, then make sure the return is UTC
                    if (d.Offset == TimeSpan.Zero && sDate[sDate.Length - 1] == 'Z') returnUTC = true;
                }
            }
        }

        if (isDateTimeOffset)
        {
            return d;
        }
        else
        {
            if (returnUTC)
            {
                return d.UtcDateTime;
            }
            else
            {
                //return the raw time passed in, forcing it to the "Local" Kind
                //for example:
                //"2020-03-27T12:00:00"       --> use 2020-03-27 12:00:00PM with Kind=Local
                //"2020-03-27T12:00:00-05:00" --> use 2020-03-27 12:00:00PM with Kind=Local
                return DateTime.SpecifyKind(d.DateTime, DateTimeKind.Local); //this will pull the raw time and force the Kind to "Local"
            }
        }
    }
}


//Date converter for JSON payloads
public class UtcDateTimeConverterJSON : DateTimeConverterBase
{
    public override bool CanRead
    {
        get
        {
            return true;
        }
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.Value == null || reader.TokenType == JsonToken.Date) return reader.Value;
        if (reader.TokenType != JsonToken.String) throw new Exception("Cannot parse Date");
    
        return UtcDateTimeConverterURI.StringToDate(objectType, (string)reader.Value, reader.Path);
    }
}
Brain2000
  • 4,655
  • 2
  • 27
  • 35