Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make ASP.Net MVC model binder treat incoming date as UTC?

I'm posting an object to an MVC controller. The object contains a field called StartDt and on the client it is a javascript Date object in local time.

When I call JSON.stringify on the object and POST it to the server using jQuery's ajax method I can see in Firebug that what's being sent to the server is an ISO string like "1900-12-31T13:00:00.000Z" which I believe should be the local time in UTC format.

When I look at the DateTime field in my controller though, it looks like its back to local time and not UTC. How can I fix this?

I want to store the UTC version of the Date that came from the client.

like image 271
C.J. Avatar asked Apr 24 '12 07:04

C.J.


People also ask

How do you convert DateTime to UTC?

The ToUniversalTime method converts a DateTime value from local time to UTC. To convert the time in a non-local time zone to UTC, use the TimeZoneInfo. ConvertTimeToUtc(DateTime, TimeZoneInfo) method. To convert a time whose offset from UTC is known, use the ToUniversalTime method.

What is custom model binder in MVC?

Custom Model Binder provides a mechanism using which we can map the data from the request to our ASP.NET MVC Model.


2 Answers

I found a gist on Google with code for an ISO 8601-compliant DateTime Model Binder, and then modified it like this:

public class DateTimeBinder : DefaultModelBinder {     public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)     {         var name = bindingContext.ModelName;         var value = bindingContext.ValueProvider.GetValue(name);         if (value == null)              return null;          DateTime date;         if (DateTime.TryParse(value.AttemptedValue, null, DateTimeStyles.RoundtripKind, out date))             return date;         else             return base.BindModel(controllerContext, bindingContext);     } } 

I believe the gist code is too restrictive - it wants 6 decimal places on seconds or it will not accept the timestamp. This uses TryParse instead of TryParseExact, so it will technically accept a LOT of timestamp types. The important part is that it uses the DateTimeStyles.RoundtripKind to respect the time zone implied by the Z. So this is no longer technically an ISO 8601-specific implementation.

You could then hook this into the MVC pipeline with a model binder attribute or with this snippet in an App_Start:

var dateTimeBinder = new DateTimeBinder();  ModelBinders.Binders.Add(typeof(DateTime), dateTimeBinder); ModelBinders.Binders.Add(typeof(DateTime?), dateTimeBinder); 
like image 30
David Boike Avatar answered Sep 18 '22 15:09

David Boike


This problem persists in ASP.NET Core 2.0. The following code will resolve it, supporting ISO 8601 basic and extended formats, properly preserving the value and setting DateTimeKind correctly. This aligns with the default behavior of JSON.Net's parsing, so it keeps your model binding behavior aligned with the rest of the system.

First, add the following model binder:

public class DateTimeModelBinder : IModelBinder {     private static readonly string[] DateTimeFormats = { "yyyyMMdd'T'HHmmss.FFFFFFFK", "yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK" };      public Task BindModelAsync(ModelBindingContext bindingContext)     {         if (bindingContext == null)             throw new ArgumentNullException(nameof(bindingContext));          var stringValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;          if (bindingContext.ModelType == typeof(DateTime?) && string.IsNullOrEmpty(stringValue))         {             bindingContext.Result = ModelBindingResult.Success(null);             return Task.CompletedTask;         }          bindingContext.Result = DateTime.TryParseExact(stringValue, DateTimeFormats,             CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var result)             ? ModelBindingResult.Success(result)             : ModelBindingResult.Failed();          return Task.CompletedTask;     } } 

Then add the following model binder provider:

public class DateTimeModelBinderProvider : IModelBinderProvider {     public IModelBinder GetBinder(ModelBinderProviderContext context)     {         if (context == null)             throw new ArgumentNullException(nameof(context));          if (context.Metadata.ModelType != typeof(DateTime) &&             context.Metadata.ModelType != typeof(DateTime?))             return null;          return new BinderTypeModelBinder(typeof(DateTimeModelBinder));     } } 

Then register the provider in your Startup.cs file:

public void ConfigureServices(IServiceCollection services) {     services.AddMvc(options =>     {         ...          options.ModelBinderProviders.Insert(0, new DateTimeModelBinderProvider());          ...     } } 
like image 132
Matt Johnson-Pint Avatar answered Sep 21 '22 15:09

Matt Johnson-Pint