c# - Cómo lidiar elegantemente con zonas horarias
timezone c# (6)
Después de varias evaluaciones, esta es mi solución final, que creo que es simple y limpia y cubre los problemas de ahorro de luz diurna.
1 - Manejamos la conversión a nivel de modelo. Entonces, en la clase Modelo, escribimos:
public class Quote
{
...
public DateTime DateCreated
{
get { return CRM.Global.ToLocalTime(_DateCreated); }
set { _DateCreated = value.ToUniversalTime(); }
}
private DateTime _DateCreated { get; set; }
...
}
2 - En un asistente global, hacemos nuestra función personalizada "ToLocalTime":
public static DateTime ToLocalTime(DateTime utcDate)
{
var localTimeZoneId = "China Standard Time";
var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById(localTimeZoneId);
var localTime = TimeZoneInfo.ConvertTimeFromUtc(utcDate, localTimeZone);
return localTime;
}
3 - Podemos mejorar esto aún más, al guardar la identificación de la zona horaria en cada perfil de usuario para que podamos recuperar de la clase de usuario en lugar de utilizar la "Hora estándar de China" constante:
public class Contact
{
...
public string TimeZone { get; set; }
...
}
4 - Aquí podemos obtener la lista de la zona horaria para mostrar al usuario para seleccionar desde un menú desplegable:
public class ListHelper
{
public IEnumerable<SelectListItem> GetTimeZoneList()
{
var list = from tz in TimeZoneInfo.GetSystemTimeZones()
select new SelectListItem { Value = tz.Id, Text = tz.DisplayName };
return list;
}
}
Entonces, ahora a las 9:25 AM en China, sitio web alojado en EE. UU., Fecha guardada en UTC en la base de datos, aquí está el resultado final:
5/9/2013 6:25:58 PM (Server - in USA)
5/10/2013 1:25:58 AM (Database - Converted UTC)
5/10/2013 9:25:58 AM (Local - in China)
EDITAR
Gracias a Matt Johnson por señalar las partes débiles de la solución original, y lo siento por eliminar la publicación original, pero tuve problemas para obtener el formato de visualización de código correcto ... resultó que el editor tenía problemas para mezclar "viñetas" con "código previo", así que eliminó las viñetas y estaba bien.
Tengo un sitio web alojado en una zona horaria diferente a la de los usuarios que usan la aplicación. Además de esto, los usuarios pueden tener una zona horaria específica. Me preguntaba cómo otros usuarios y aplicaciones de SO se acercan a esto. La parte más obvia es que dentro de la base de datos, la fecha / horas se almacenan en UTC. Cuando está en el servidor, todas las fechas / horas deben tratarse en UTC. Sin embargo, veo tres problemas que trato de superar:
Obteniendo la hora actual en UTC (resuelto fácilmente con
DateTime.UtcNow
).Tirando fecha / horas de la base de datos y mostrándolas al usuario. Hay potencialmente muchas llamadas para imprimir fechas en diferentes vistas. Estaba pensando en alguna capa entre la vista y los controladores que podría resolver este problema. O tener un método de extensión personalizado en
DateTime
(ver a continuación). El inconveniente principal es que en cada ubicación de uso de una fecha y hora en una vista, se debe llamar al método de extensión.Esto también agregaría dificultad para usar algo como el
JsonResult
. Ya no podría llamar fácilmente aJson(myEnumerable)
, tendría que serJson(myEnumerable.Select(transformAllDates))
. Tal vez AutoMapper podría ayudar en esta situación?Obtener información del usuario (Local a UTC). Por ejemplo, POSTING un formulario con una fecha requeriría convertir la fecha a UTC antes. Lo primero que se me ocurre es crear un
ModelBinder
personalizado.
Aquí están las extensiones que pensé usar en las vistas:
public static class DateTimeExtensions
{
public static DateTime UtcToLocal(this DateTime source,
TimeZoneInfo localTimeZone)
{
return TimeZoneInfo.ConvertTimeFromUtc(source, localTimeZone);
}
public static DateTime LocalToUtc(this DateTime source,
TimeZoneInfo localTimeZone)
{
source = DateTime.SpecifyKind(source, DateTimeKind.Unspecified);
return TimeZoneInfo.ConvertTimeToUtc(source, localTimeZone);
}
}
Creo que lidiar con zonas horarias sería algo tan común considerando que muchas aplicaciones ahora están basadas en la nube, donde la hora local del servidor podría ser muy diferente a la zona horaria esperada.
¿Esto ha sido resuelto con elegancia antes? ¿Hay algo que me estoy perdiendo? Ideas y pensamientos son muy apreciados.
EDITAR: Para aclarar algo de confusión, pensé en agregar algunos detalles más. El problema ahora no es cómo almacenar las horas UTC en el db, sino más bien sobre el proceso de pasar de UTC-> Local y Local-> UTC. Como señala @Max Zerbini, obviamente es inteligente poner el código UTC-> Local en la vista, pero ¿está usando DateTimeExtensions
realmente como respuesta? Al recibir información del usuario, ¿tiene sentido aceptar fechas como la hora local del usuario (dado que eso es lo que usaría JS) y luego usar un ModelBinder
para transformarlo a UTC? La zona horaria del usuario se almacena en la base de datos y se recupera fácilmente.
En la sección de eventos en sf4answers , los usuarios ingresan una dirección para un evento, así como una fecha de inicio y una fecha de finalización opcional. Estos tiempos se traducen en un datetimeoffset
en el servidor SQL que representa el desplazamiento de UTC.
Este es el mismo problema al que se enfrenta (aunque está tomando un enfoque diferente, ya que está usando DateTime.UtcNow
); tiene una ubicación y necesita traducir una hora de una zona horaria a otra.
Hay dos cosas principales que hice que funcionaron para mí. Primero, use la estructura DateTimeOffset
, siempre. Cuenta para compensación de UTC y si puede obtener esa información de su cliente, hace su vida un poco más fácil.
En segundo lugar, al realizar las traducciones, suponiendo que conoce la ubicación / zona horaria en la que se encuentra el cliente, puede usar la base de datos de zona horaria de información pública para traducir una hora de UTC a otra zona horaria (o triangular, si lo desea, entre dos zonas horarias). Lo mejor de la base de datos tz (a veces denominada base de datos Olson ) es que da cuenta de los cambios en las zonas horarias a lo largo de la historia; obtener una compensación es una función de la fecha en la que desea obtener la compensación (basta con observar la Ley de Política Energética de 2005 que cambió las fechas en que entró en vigencia el horario de verano en los EE . UU .).
Con la base de datos a la mano, puede usar la API .NET de ZoneInfo (base de datos tz / Olson) . Tenga en cuenta que no hay una distribución binaria, tendrá que descargar la última versión y compilarla usted mismo.
En el momento de escribir estas líneas, actualmente analiza todos los archivos en la última distribución de datos (en realidad lo ejecuté contra el archivo ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz el 25 de septiembre, 2011, en marzo de 2017, lo obtendría a través de https://iana.org/time-zones o de ftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gz ).
Entonces en sf4answers, después de obtener la dirección, se geocodifica en una combinación de latitud / longitud y luego se envía a un servicio web de terceros para obtener una zona horaria que corresponde a una entrada en la base de datos tz. A partir de ahí, las horas de inicio y finalización se convierten en instancias DateTimeOffset
con el desplazamiento UTC adecuado y luego se almacenan en la base de datos.
En cuanto a tratar con SO y sitios web, depende de la audiencia y de lo que intenta mostrar. Si lo nota, la mayoría de los sitios web sociales (y SO, y la sección de eventos en sf4answers) muestran los eventos en tiempo relativo o, si se usa un valor absoluto, por lo general es UTC.
Sin embargo, si su audiencia espera horas locales, usar DateTimeOffset
junto con un método de extensión que tome la zona horaria para convertir a sería bien; el tipo de datos SQL datetimeoffset
se traduciría a .NET DateTimeOffset
que luego puede obtener la hora universal para usar el método GetUniversalTime
. Desde allí, simplemente use los métodos en la clase ZoneInfo
para convertir desde UTC a la hora local (tendrá que trabajar un poco para ponerlo en DateTimeOffset
, pero es lo suficientemente simple de hacer).
¿Dónde hacer la transformación? Ese es un costo que tendrá que pagar en algún lugar , y no hay una "mejor" manera. Sin embargo, optaría por la vista, con el desplazamiento de la zona horaria como parte del modelo de vista presentado a la vista. De esta forma, si cambian los requisitos para la vista, no tiene que cambiar su modelo de vista para acomodar el cambio. Su JsonResult
simplemente contendría un modelo con IEnumerable<T>
y el desplazamiento.
En el lado de entrada, ¿usando una carpeta modelo? Yo diría absolutamente de ninguna manera. No puede garantizar que todas las fechas (ahora o en el futuro) deban transformarse de esta manera, debe ser una función explícita de su controlador para realizar esta acción. Una vez más, si los requisitos cambian, no tiene que modificar una o varias instancias de ModelBinder
para ajustar su lógica comercial; y es lógica de negocios, lo que significa que debe estar en el controlador.
Esta es solo mi opinión, creo que la aplicación MVC debería separar el problema de la presentación de datos del pozo de la gestión del modelo de datos. Una base de datos puede almacenar datos en el tiempo del servidor local, pero es un deber de la capa de presentación procesar la fecha y hora utilizando la zona horaria del usuario local. Esto me parece el mismo problema que I18N y el formato de número para diferentes países. En su caso, su aplicación debe detectar la Culture
y la zona horaria del usuario y cambiar la vista que muestra texto diferente, número y presentación en tiempo real, pero los datos almacenados pueden tener el mismo formato.
No es que sea una recomendación, es más compartir un paradigma, pero la forma más agresiva que he visto de manejar la información de zona horaria en una aplicación web (que no es exclusiva de ASP.NET MVC) fue la siguiente:
Todos los horarios en el servidor son UTC. Eso significa usar, como usted dijo,
DateTime.UtcNow
.Intente no confiar lo menos posible en las fechas de transferencia del cliente al servidor. Por ejemplo, si necesita "ahora", no cree una fecha en el cliente y luego páselo al servidor. Cree una fecha en su GET y páselo al ViewModel o en POST haga
DateTime.UtcNow
.
Hasta el momento, tarifa bastante estándar, pero aquí es donde las cosas se ponen ''interesantes''.
Si tiene que aceptar una fecha del cliente, utilice javascript para asegurarse de que los datos que está publicando en el servidor están en UTC. El cliente sabe en qué zona horaria se encuentra, por lo que puede con una precisión razonable convertir las horas en UTC.
Al renderizar vistas, usaban el elemento HTML5
<time>
, nunca procesaban las fechas directamente en ViewModel. Se implementó como extensiónHtmlHelper
, algo así comoHtml.Time(Model.when)
. Haría que<time datetime=''[utctime]'' data-date-format=''[datetimeformat]''></time>
.Luego utilizarían javascript para traducir la hora UTC en la hora local del cliente. El script encontraría todos los elementos
<time>
y usaría la propiedad datadate-format
data para formatear la fecha y completar el contenido del elemento.
De esta forma, nunca tuvieron que realizar un seguimiento, almacenar o administrar la zona horaria de un cliente. Al servidor no le importaba la zona horaria en la que se encontraba el cliente, ni tenía que hacer ninguna conversión de zona horaria. Simplemente escupió UTC y permitió que el cliente convirtiera eso en algo que fuera razonable. Lo cual es fácil desde el navegador, ya que sabe en qué zona horaria se encuentra. Si el cliente cambió su zona horaria, la aplicación web se actualizaría automáticamente. Lo único que almacenaron fue la cadena de formato de fecha y hora para la configuración regional del usuario.
No digo que fuera el mejor enfoque, pero era diferente y no lo había visto antes. Tal vez obtendrás algunas ideas interesantes de ello.
Para la salida, crea una plantilla de pantalla / editor como esta
@inherits System.Web.Mvc.WebViewPage<System.DateTime>
@Html.Label(Model.ToLocalTime().ToLongTimeString()))
Puede vincularlos según los atributos de su modelo si solo desea que ciertos modelos utilicen esas plantillas.
Consulte here y here para obtener más detalles sobre cómo crear plantillas de editor personalizadas.
Alternativamente, dado que desea que funcione tanto para la entrada como para la salida, le sugiero que extienda un control o incluso que cree el suyo propio. De esta forma, puede interceptar tanto la entrada como las salidas y convertir el texto / valor según sea necesario.
Esperamos que este enlace te empuje en la dirección correcta si quieres seguir ese camino.
De cualquier manera, si quieres una solución elegante, va a ser un poco de trabajo. En el lado positivo, una vez que lo haya hecho una vez, puede guardarlo en la biblioteca de códigos para usarlo en el futuro.
Probablemente sea un mazo para romper una tuerca, pero podría inyectar una capa entre las capas UI y Business, que convierte de forma transparente las fechas a la hora local en los gráficos de objetos devueltos, y a UTC en los parámetros de fecha y hora de entrada.
Me imagino que esto podría lograrse usando PostSharp o algún contenedor de inversión de control.
Personalmente, me limitaría a convertir explícitamente tus fechas en la interfaz de usuario ...