How to make ASP.Net MVC model binder treat incoming date as UTC?
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());
...
}
}
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);
I created this little attribute.
public class ConvertDateToUTCAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var dateArgs =
filterContext.ActionParameters.Where(
x => x.Value != null && x.Value.GetType().IsAssignableFrom(typeof(DateTime))).ToList();
foreach (var keyValuePair in dateArgs)
{
var date = (DateTime) keyValuePair.Value;
if (date.Kind == DateTimeKind.Local)
filterContext.ActionParameters[keyValuePair.Key] = date.ToUniversalTime();
}
base.OnActionExecuting(filterContext);
}
}
So this will leave dates that are Unspecified or already Utc alone. You can apply it the whole controller.
Alternatively, you can specify your bound objects to be DateTimeOffset
rather than DateTime
, and there will be no automatic conversion. As long as the incoming string has the Z
suffix, you should get the original date with an offset of +00:00
.
You may have to use the DateTime.ToUniversalTime() method to get back the UTC time.