web-dev-qa-db-fra.com

Comment gérer avec élégance les fuseaux horaires

J'ai un site Web hébergé dans un fuseau horaire différent de celui des utilisateurs utilisant l'application. En plus de cela, les utilisateurs peuvent avoir un fuseau horaire spécifique. Je me demandais comment d'autres SO utilisateurs et applications le traitaient? La partie la plus évidente est que, dans la base de données, la date/l'heure sont stockées en UTC. Sur le serveur, toutes les date/heures doivent être Je vois cependant trois problèmes que j'essaie de surmonter:

  1. Obtenir l'heure actuelle en UTC (résolu facilement avec DateTime.UtcNow).

  2. Extraire les dates/heures de la base de données et les afficher à l'utilisateur. Il y a potentiellement beaucoup d'appels pour imprimer des dates sur différentes vues. Je pensais à une couche entre la vue et les contrôleurs pouvant résoudre ce problème. Ou avoir une méthode d'extension personnalisée sur DateTime (voir ci-dessous). Le principal inconvénient est que, à à chaque emplacement d'utilisation d'une date/heure dans une vue, la méthode d'extension doit être appelée!

    Cela ajouterait également de la difficulté à utiliser quelque chose comme le JsonResult. Vous ne pourriez plus facilement appeler Json(myEnumerable), il faudrait que ce soit Json(myEnumerable.Select(transformAllDates)). Peut-être qu'AutoMapper pourrait aider dans cette situation?

  3. Obtenir les commentaires de l'utilisateur (du local au UTC). Par exemple, POST un formulaire avec une date nécessiterait la conversion de la date au format UTC avant. La première chose qui me vient à l’esprit est la création d’un ModelBinder personnalisé.

Voici les extensions que j'ai pensé utiliser dans les vues:

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

Je penserais que traiter avec les fuseaux horaires serait une chose si courante étant donné que beaucoup d'applications sont maintenant basées sur le cloud où l'heure locale du serveur pourrait être très différente de celle du fuseau horaire prévu.

Est-ce que cela a été élégamment résolu avant? Y a-t-il quelque chose qui me manque? Les idées et les pensées sont très appréciées.

EDIT: Pour dissiper toute confusion, j'ai pensé ajouter quelques détails supplémentaires. Le problème actuel n’est pas comment enregistrer les heures UTC dans la base de données, mais plutôt le processus de passage de UTC-> Local et Local-> UTC. Comme le souligne @ Max Zerbini, il est évidemment judicieux de mettre le code UTC-> Local dans la vue, mais l'utilisation de DateTimeExtensions est-elle vraiment la solution? Lors de la saisie de l'utilisateur, est-il judicieux d'accepter les dates comme heure locale de l'utilisateur (puisque c'est ce que JS utiliserait), puis d'utiliser un ModelBinder pour le convertir en UTC? Le fuseau horaire de l'utilisateur est stocké dans la base de données et est facilement récupérable.

132
TheCloudlessSky

Il ne s’agit pas d’une recommandation mais plutôt d’un partage de paradigme, mais c’est la manière la plus agressive que j’ai vue de gérer les informations de fuseau horaire dans une application Web (qui n’est pas exclusive à ASP.NET MVC): le suivant:

  • Toutes les heures sur le serveur sont en UTC. Cela signifie utiliser, comme vous l'avez dit, DateTime.UtcNow.

  • Essayez de ne pas faire confiance le moins possible aux dates de passage des clients sur le serveur. Par exemple, si vous avez besoin de "maintenant", ne créez pas de date sur le client, puis transmettez-la au serveur. Créez une date dans votre GET et transmettez-la au ViewModel ou sur POST do DateTime.UtcNow.

Jusqu'ici, le tarif est assez standard, mais c'est là que les choses deviennent "intéressantes".

  • Si vous devez accepter une date du client, utilisez javascript pour vous assurer que les données que vous publiez sur le serveur sont au format UTC. Le client sait dans quel fuseau horaire il se trouve, il peut donc convertir les heures au format UTC avec une précision raisonnable.

  • Lors du rendu des vues, ils utilisaient l'élément HTML5 <time>, Ils ne rendraient jamais les dates/heures directement dans le ViewModel. Il a été implémenté comme extension HtmlHelper, quelque chose comme Html.Time(Model.when). Cela rendrait <time datetime='[utctime]' data-date-format='[datetimeformat]'></time>.

    Ensuite, ils utiliseraient javascript pour traduire l'heure UTC en heure locale du client. Le script rechercherait tous les éléments <time> Et utiliserait la propriété de données date-format Pour formater la date et renseigner le contenu de l'élément.

Ainsi, ils ne doivent jamais suivre, stocker ou gérer le fuseau horaire d'un client. Le serveur ne se souciait pas du fuseau horaire du client ni de la conversion des fuseaux horaires. Il crache simplement le temps UTC et laisse le client convertir cela en quelque chose de raisonnable. Ce qui est facile avec le navigateur, car il sait dans quel fuseau horaire il se trouve. Si le client modifiait son fuseau horaire, l’application Web se mettrait automatiquement à jour. La seule chose qu'ils ont stockée était la chaîne de formatage datetime pour les paramètres régionaux de l'utilisateur.

Je ne dis pas que c'était la meilleure approche, mais c'était une approche différente que je n'avais jamais vue auparavant. Peut-être y trouverez-vous des idées intéressantes.

101
J. Holmes

Après plusieurs retours, voici ma solution finale qui, à mon avis, est propre et simple et couvre les problèmes de l'heure d'été.

1 - Nous traitons la conversion au niveau du modèle. Donc, dans la classe Model, nous écrivons:

    public class Quote
    {
        ...
        public DateTime DateCreated
        {
            get { return CRM.Global.ToLocalTime(_DateCreated); }
            set { _DateCreated = value.ToUniversalTime(); }
        }
        private DateTime _DateCreated { get; set; }
        ...
    }

2 - Dans une aide globale, nous créons notre fonction personnalisée "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 - Nous pouvons améliorer cela davantage en enregistrant l'identifiant du fuseau horaire dans chaque profil utilisateur afin de pouvoir l'extraire de la classe d'utilisateurs au lieu d'utiliser la constante "Heure normale de la Chine":

public class Contact
{
    ...
    public string TimeZone { get; set; }
    ...
}

4 - Ici, nous pouvons obtenir la liste des fuseaux horaires à montrer à l'utilisateur pour le sélectionner dans une liste déroulante:

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

Donc, maintenant à 9h25 en Chine, site hébergé aux Etats-Unis, date enregistrée en UTC dans la base de données, voici le résultat 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)

[~ # ~] éditer [~ # ~]

Merci à Matt Johnson pour avoir signalé les points faibles de la solution originale, et désolé pour la suppression de l'article d'origine, mais des problèmes d'obtention du format d'affichage du code correct ... s'est avéré que l'éditeur avait des problèmes pour mélanger les "puces" avec "pré-code", alors j'ai enlevé les bulles et c'était ok.

13
Nestor

Dans la section des événements sur sf4answers , les utilisateurs saisissent une adresse pour un événement, ainsi qu'une date de début et une date de fin facultative. Ces heures sont traduites en datetimeoffset dans le serveur SQL qui comptabilise le décalage par rapport à l'UTC.

C’est le même problème que vous rencontrez (bien que vous adoptiez une approche différente, en ce sens que vous utilisez DateTime.UtcNow ); vous avez un emplacement et vous devez convertir une heure d'un fuseau horaire à un autre.

J'ai fait deux choses principales qui ont fonctionné pour moi. Premièrement, utilisez DateTimeOffset structure , toujours. Cela représente un décalage par rapport à UTC et si vous pouvez obtenir cette information de votre client, cela vous facilitera la vie.

Deuxièmement, lors de la traduction, en supposant que vous connaissiez l'emplacement/le fuseau horaire dans lequel se trouve le client, vous pouvez utiliser la base de données des informations du public sur le fuseau horaire pour convertir une heure de l'heure UTC en un autre fuseau horaire (ou trianguler). , si vous voulez, entre deux fuseaux horaires). Le point fort de la base de données tz (parfois appelée base de données Olson ) est qu’elle rend compte des modifications apportées aux fuseaux horaires au cours de l’histoire; l'obtention d'un décalage est fonction de la date à laquelle vous souhaitez obtenir le décalage (il suffit de regarder le Energy Policy Act of 2005 qui modifié les dates d'entrée en vigueur de l'heure d'été) les États-Unis ).

Avec la base de données en main, vous pouvez utiliser l’API .NET ZoneInfo (base de données tz/Olson) . Notez qu'il n'y a pas de distribution binaire, vous devrez télécharger le dernière version et le compiler vous-même.

Au moment d'écrire ces lignes, il analyse actuellement tous les fichiers de la dernière distribution de données (je l'ai en fait exécuté contre le fichier ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz fichier le 25 septembre 2011; en mars 2017, vous l'obtiendrez via https://iana.org/time-zones ou depuis ftp: //fpt.iana .org/tz/releases/tzdata2017a.tar.gz ).

Ainsi, sur sf4answers, après avoir obtenu l'adresse, il est géocodé dans une combinaison latitude/longitude puis envoyé à un service Web tiers pour obtenir un fuseau horaire qui correspond à une entrée de la base de données tz. À partir de là, les heures de début et de fin sont converties en DateTimeOffset instances avec le décalage UTC approprié, puis stockées dans la base de données.

En ce qui concerne son traitement sur SO et les sites Web, cela dépend du public cible et de ce que vous essayez d'afficher. Si vous remarquez, la plupart des sites Web sociaux (et des SO, ainsi que la section des événements sur sf4answers) afficher les événements en relatif temps, ou, si une valeur absolue est utilisée, il s'agit généralement de l'heure UTC.

Cependant, si votre auditoire s'attend à des heures locales, alors utiliser DateTimeOffset avec une méthode d'extension prenant le fuseau horaire pour la conversion serait parfait; le type de données SQL datetimeoffset se traduirait par le .NET DateTimeOffset auquel vous pourrez alors obtenir l'heure universelle pour utiliser la méthode GetUniversalTime . A partir de là, il vous suffit d'utiliser les méthodes de la classe ZoneInfo pour convertir de l'heure UTC à l'heure locale (vous devrez faire un peu de travail pour le convertir en un fichier DateTimeOffset, mais c'est assez simple faire).

Où faire la transformation? C'est un coût que vous allez devoir payer quelque part, et il n'y a pas de "meilleure" façon. Je choisirais cependant la vue, le décalage horaire étant intégré au modèle de vue présenté à la vue. Ainsi, si les exigences de la vue changent, vous n’aurez pas à modifier votre modèle de vue pour prendre en compte le changement. Votre JsonResult contiendrait simplement un modèle avec le IEnumerable<T> et le décalage.

Du côté de l'entrée, en utilisant un classeur de modèle? Je dirais absolument pas. Vous ne pouvez pas garantir que tous les dates (actuelles ou futures) devront être transformées de cette manière. Cela devrait être une fonction explicite de votre contrôleur d'effectuer cette action. Encore une fois, si les exigences changent, vous n'avez pas à modifier une ou plusieurs instances ModelBinder pour ajuster votre logique métier. et cela est la logique métier, ce qui signifie que cela devrait être dans le contrôleur.

8
casperOne

Ceci est juste mon avis, je pense que l’application MVC devrait séparer le problème de présentation des données de puits de la gestion des modèles de données. Une base de données peut stocker des données à l’heure du serveur local, mais la couche présentation a pour obligation de rendre l’heure en utilisant le fuseau horaire de l’utilisateur local. Cela me semble avoir le même problème que I18N et le format numérique pour différents pays. Dans votre cas, votre application doit détecter le Culture et le fuseau horaire de l'utilisateur, puis modifier la vue affichant un texte, un nombre et une présentation de date différents, mais les données stockées peuvent avoir le même format.

5
Max Zerbini

Pour la sortie, créez un modèle d'affichage/éditeur comme celui-ci

@inherits System.Web.Mvc.WebViewPage<System.DateTime>
@Html.Label(Model.ToLocalTime().ToLongTimeString()))

Vous pouvez les lier en fonction d'attributs de votre modèle si vous souhaitez que seuls certains modèles utilisent ces modèles.

Voir ici et ici pour plus de détails sur la création de modèles d'éditeur personnalisés.

Alternativement, puisque vous voulez que cela fonctionne à la fois en entrée et en sortie, je vous suggère d’étendre un contrôle ou même de créer le vôtre. De cette façon, vous pouvez intercepter les entrées et les sorties et convertir le texte/la valeur selon vos besoins.

Ce lien espérons-le vous pousser dans la bonne direction si vous voulez aller dans cette voie.

Quoi qu'il en soit, si vous voulez une solution élégante, cela demandera un peu de travail. Le bon côté des choses, une fois que vous avez terminé, vous pouvez le conserver dans votre bibliothèque de code pour une utilisation future!

1
dnatoli

C'est probablement un sledgehammer pour casser un écrou, mais vous pouvez injecter une couche entre les couches UI et Business qui convertit de manière transparente les dates/heures en heure locale sur les graphiques d'objet renvoyés et en UTC pour les paramètres date/heure en entrée.

J'imagine que cela pourrait être réalisé en utilisant PostSharp ou une inversion du conteneur de contrôle.

Personnellement, je voudrais simplement convertir explicitement vos dates/heures dans l'interface utilisateur ...

0
geoffreys