2012-04-24 7 views
13

Estoy publicando un objeto en un controlador MVC. El objeto contiene un campo llamado StartDt y en el cliente es un objeto de fecha de JavaScript en hora local.¿Cómo hacer que la carpeta de modelo ASP.Net MVC trate la fecha de entrada como UTC?

Cuando llamo a JSON.stringify en el objeto y LO PUBLICO al servidor utilizando el método ajax de jQuery, puedo ver en Firebug que lo que se envía al servidor es una cadena ISO como "1900-12-31T13: 00: 00.000 Z ", que creo que debería ser la hora local en formato UTC.

Sin embargo, cuando miro el campo DateTime en mi controlador, parece que está de vuelta en la hora local y no en UTC. ¿Cómo puedo arreglar esto?

Quiero almacenar la versión UTC de la Fecha que vino del cliente.

Respuesta

3

Puede que tenga que utilizar el método DateTime.ToUniversalTime() para recuperar la hora UTC.

+7

Este es el tipo de cosa que sería mejor manejada por la infraestructura, así que no tenemos que pensar en ello * cada vez * manejamos un DateTime. –

+0

@DavidBoike: http://www.martin-brennan.com/custom-utc-datetime-model-binding-mvc/ –

4

Creé este pequeño atributo.

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); 
    } 
} 

Esto dejará las fechas que no se especifiquen o que ya sean Utc. Puedes aplicarlo a todo el controlador.

+0

¿Qué pasa si estamos pasando una clase personalizada que tiene la propiedad DateTime dentro de ella? IsAssignableFrom (typeof (DateTime)) no debería funcionar. –

+1

Sí, esto solo funciona para las fechas que son parámetros directos de la acción. Una alternativa más robusta sería probablemente usar el enlace modelo. Vea la carpeta a continuación y también mi comentario al respecto. – Sam

+0

¿Por qué los votos a favor? Esta es una alternativa válida para escribir una carpeta modelo. – Sam

16

me encontré con una esencia en Google con el código de una ISO 8601 compatible DateTime Model Binder, y luego modificado así:

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); 
    } 
} 

Creo que el código de GIST es demasiado restrictivo - que quiere 6 decimales en segundos o se no aceptará la marca de tiempo. Esto usa TryParse en lugar de TryParseExact, por lo que técnicamente aceptará MUCHOS tipos de marcas de tiempo. La parte importante es que usa DateTimeStyles.RoundtripKind para respetar el huso horario implicado por Z. Por lo tanto, esto ya no es técnicamente una implementación específica de ISO 8601.

A continuación, podría enganchar esta en la tubería MVC con un atributo de modelo aglutinante o con este fragmento en un App_Start:

var dateTimeBinder = new DateTimeBinder(); 

ModelBinders.Binders.Add(typeof(DateTime), dateTimeBinder); 
ModelBinders.Binders.Add(typeof(DateTime?), dateTimeBinder); 
+4

No es necesario analizar el valor de fecha y hora. Como se indica en la pregunta, el problema no es el análisis sintáctico de la fecha (su análisis es perfecto) sino el comportamiento del archivador de modelo predeterminado para convertir la fecha utc analizada en el servidor local datetime. Estoy de acuerdo en que una carpeta modelo es probablemente una mejor opción que un filtro de acción, sin embargo, todo lo que necesita es obtener el valor de la llamada base.BindModel, verificar si el valor es Kind = Local, luego convertirlo a Utc y regresar. – Sam

+1

Bien, me pregunto por qué .NET no analiza las fechas ISO con 'DateTimeStyles.RoundtripKind' de manera predeterminada. –

+0

Sin conocer la imagen completa, diría que esto debería ser el predeterminado. Ahora cuando envío cadenas en ISO 8601, se analizan correctamente y las conversiones a la hora local del servidor también son correctas. – beruic

2

Este problema persiste en ASP.NET Core 2.0. El siguiente código lo resolverá, admitiendo los formatos básicos y extendidos ISO 8601, conservando correctamente el valor y configurando DateTimeKind correctamente. Esto se alinea con el comportamiento predeterminado del análisis de JSON.Net, por lo que mantiene el comportamiento de enlace de su modelo alineado con el resto del sistema.

En primer lugar, añadir el siguiente modelo de aglutinante:

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; 
    } 
} 

A continuación, añada el siguiente proveedor de modelo de ligante:

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)); 
    } 
} 

luego registrar el proveedor en su archivo Startup.cs:

public void ConfigureServices(IServiceCollection services) 
{ 
    services.AddMvc(options => 
    { 
     ... 

     options.ModelBinderProviders.Insert(0, new DateTimeModelBinderProvider()); 

     ... 
    } 
} 
+0

Esto es genial. Parece que al hacer 'ConfigureServices', el orden de los asuntos' ModelBinderProviders', supongo que es por eso que estás haciendo un Insert en lugar de un Add to the collection. –

Cuestiones relacionadas