Comment gérer élégamment les fuseaux horaires

140

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 les autres utilisateurs et applications SO abordent cela? La partie la plus évidente est qu'à l'intérieur de la base de données, la date / l'heure sont stockées en UTC. Sur le serveur, toutes les dates / heures doivent être traitées en UTC. Cependant, je vois 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 qui pourraient résoudre ce problème. Ou avoir une méthode d'extension personnalisée DateTime(voir ci-dessous). L'inconvénient majeur est qu'à chaque emplacement d'utilisation d'un datetime 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 appeler facilement Json(myEnumerable), il faudrait que ce soit le cas Json(myEnumerable.Select(transformAllDates)). Peut-être qu'AutoMapper pourrait vous aider dans cette situation?

  3. Obtention de l'entrée de l'utilisateur (local à UTC). Par exemple, publier un formulaire avec une date nécessiterait de convertir la date en UTC avant. La première chose qui me vient à l'esprit est de créer une coutume ModelBinder.

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 pense que gérer les fuseaux horaires serait une chose si courante étant donné que de nombreuses applications sont désormais basées sur le cloud, où l'heure locale du serveur pourrait être très différente du fuseau horaire prévu.

Cela a-t-il été résolu avec élégance auparavant? 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 une certaine confusion, j'ai pensé ajouter quelques détails supplémentaires. Le problème à l'heure actuelle n'est pas de savoir comment stocker 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 est-ce DateTimeExtensionsvraiment la réponse? Lors de l'obtention des entrées 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 a ModelBinderpour transformer en UTC? Le fuseau horaire de l'utilisateur est stocké dans la base de données et est facilement récupéré.

TheCloudlessSky
la source
3
Vous pourriez d'abord lire cet excellent article ... Meilleures pratiques en
matière d'
@dodgy_coder - Cela a toujours été un excellent lien de ressources pour les fuseaux horaires. Cependant, cela ne résout vraiment aucun de mes problèmes (en particulier concernant MVC). Merci quand même.
TheCloudlessSky
code.google.com/p/noda-time pourrait être utile
Arnis Lapsa
Curieux de savoir quelle solution vous avez optée. Face à une décision similaire moi-même. Bonne question.
Sean
@Sean - La solution jusqu'à présent n'a pas été élégante (c'est pourquoi je n'ai pas encore accepté de réponse). Il y a eu beaucoup de surcharge manuelle d'impression des dates / heures et de leur conversion manuelle avec un fichier ModelBinder.
TheCloudlessSky

Réponses:

106

Non pas que ce soit une recommandation, il partage plus un paradigme, mais 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) était la suivante:

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

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

Jusqu'à présent, un tarif 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 en UTC. Le client sait dans quel fuseau horaire il se trouve, il peut donc, avec une précision raisonnable, convertir les heures en UTC.

  • Lors du rendu des vues, ils utilisaient l' <time>élément HTML5 , ils ne rendraient jamais les datetimes directement dans le ViewModel. Il a été implémenté comme une HtmlHelperextension, 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 trouverait tous les <time>éléments et utiliserait la date-formatpropriété data pour formater la date et remplir le contenu de l'élément.

De cette façon, ils n'ont jamais eu à suivre, stocker ou gérer le fuseau horaire d'un client. Le serveur ne se souciait pas du fuseau horaire dans lequel se trouvait le client et n'avait pas à effectuer de traductions de fuseau horaire. Il crache simplement UTC et laisse le client le convertir en quelque chose de raisonnable. Ce qui est facile à partir du 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 stockaient était la chaîne de format 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 que vous en tirerez des idées intéressantes.

J. Holmes
la source
Merci pour votre réponse. Lors de l'obtention de la date d'entrée de l'utilisateur (par exemple, la planification d'un rendez-vous), cette entrée n'est-elle pas supposée être dans son fuseau horaire actuel? Que pensez-vous du ModelBinder faisant cette transformation avant d'entrer dans l'action (voir mes commentaires à @casperOne. De plus, l' <time>idée est plutôt cool. Le seul inconvénient est que cela signifie que je dois interroger l'ensemble du DOM pour ces éléments, ce qui n'est pas vraiment sympa juste de transformer les dates (qu'en est-il de JS désactivé?). Merci encore!
TheCloudlessSky
Habituellement, oui, l'hypothèse est que ce serait dans un fuseau horaire local. Mais ils ont tenu à s'assurer qu'à chaque fois qu'ils collectaient une heure d'un utilisateur, elle était transformée en UTC en utilisant javascript avant qu'elle ne soit envoyée au serveur. Leur solution était très lourde en JS et ne se dégradait pas très bien lorsque JS était éteint. Je n'y ai pas suffisamment réfléchi pour voir s'il y a un malin autour de ça. Mais comme je l'ai dit, peut-être que cela vous donnera des idées.
J. Holmes
2
J'ai depuis travaillé sur un autre projet qui nécessitait des dates locales et c'est la méthode que j'ai utilisée. J'utilise moment.js pour faire le formatage de la date / heure ... c'est gentil. Merci!
TheCloudlessSky
N'existe-t-il pas un moyen simple de définir dans l'app.config / web.cofig de l'application un fuseau horaire d'application et de le faire transformer toutes les valeurs DateTime.Now en DateTime.UtcNow? (Je veux éviter les situations où les programmeurs de l'entreprise utiliseraient toujours DateTime.Maintenant par erreur)
Uri Abramson
3
Un inconvénient de cette approche que je peux voir est que lorsque vous utilisez le temps pour d'autres choses, créez des pdf, des e-mails, etc., il n'y a pas d'élément de temps pour ceux-ci, vous devrez donc toujours les convertir manuellement. Sinon, solution très soignée
shenku
15

Après plusieurs retours, voici ma solution finale que je trouve propre et simple et qui couvre les problèmes d'heure d'été.

1 - Nous gérons 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 un helper global 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 encore améliorer cela, en enregistrant l'identifiant du fuseau horaire dans chaque profil utilisateur afin que nous puissions récupérer de la classe d'utilisateurs au lieu d'utiliser la constante "Heure standard de Chine":

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

4 - Ici, nous pouvons obtenir la liste des fuseaux horaires à montrer à l'utilisateur pour 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 Web hébergé aux USA, 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 d' avoir souligné les points faibles de la solution originale, et désolé d'avoir supprimé le message original, mais j'ai eu des problèmes pour obtenir le bon format d'affichage du code ... il s'est avéré que l'éditeur a des problèmes pour mélanger les "puces" avec le "pré-code", alors je enlevé les bulles et c'était ok.

Nestor
la source
Cela semble réalisable si quelqu'un n'a pas d'architecture n-tiers ou si les bibliothèques Web sont partagées. Comment nous pouvons partager l'identifiant du fuseau horaire avec la couche de données.
Vikash Kumar
9

Dans la section é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 un datetimeoffsetserveur SQL qui tient compte du décalage par rapport à UTC.

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

Il y a deux choses principales que j'ai faites qui ont fonctionné pour moi. Tout d'abord, utilisez toujours la DateTimeOffsetstructure . Cela représente un décalage par rapport à UTC et si vous pouvez obtenir ces informations de votre client, cela vous facilite un peu la vie.

Deuxièmement, lorsque vous effectuez les traductions, en supposant que vous connaissez l'emplacement / le fuseau horaire dans lequel se trouve le client, vous pouvez utiliser la base de données de fuseaux horaires d'informations publiques pour traduire une heure UTC vers un autre fuseau horaire (ou trianguler, si vous voulez, entre deux fuseaux horaires). L'avantage de la base de données tz (parfois appelée base de données Olson ) est qu'elle tient compte des changements de fuseaux horaires au cours de l'histoire; obtenir un décalage est fonction de la date à laquelle vous souhaitez obtenir le décalage (il suffit de regarder la loi de 2005 sur la politique énergétique qui a modifié les dates d'entrée en vigueur de l'heure d'été aux États-Unis ).

Avec la base de données en main, vous pouvez utiliser l' API .NET ZoneInfo (base de données tz / base de données Olson) .NET . Notez qu'il n'y a pas de distribution binaire, vous devrez télécharger la dernière version et la 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é avec le fichier ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz le 25 septembre 2011; en mars 2017, vous l'obteniez via https://iana.org/time-zones ou sur ftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gz ).

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

Quant à la traiter sur SO et sur les sites Web, cela dépend de l'audience et de ce que vous essayez d'afficher. Si vous remarquez, la plupart des sites Web sociaux (et SO, et la section des événements sur sf4answers) affichent les événements en temps relatif , ou, si une valeur absolue est utilisée, c'est généralement UTC.

Cependant, si votre public s'attend à des heures locales, utiliser DateTimeOffsetavec une méthode d'extension qui prend le fuseau horaire à convertir serait très bien; le type de données SQL datetimeoffsetse traduirait par le .NET DateTimeOffsetque vous pouvez alors obtenir le temps universel pour utiliser la GetUniversalTimeméthode . À partir de là, vous utilisez simplement les méthodes de la ZoneInfoclasse pour convertir l'heure UTC en heure locale (vous devrez faire un peu de travail pour la mettre en a 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 «meilleur» moyen. J'opterais cependant pour la vue, avec le décalage du fuseau horaire dans le cadre du modèle de vue présenté à la vue. De cette façon, si les exigences de la vue changent, vous n'avez pas à modifier votre modèle de vue pour s'adapter à la modification. Votre JsonResultcontiendrait simplement un modèle avec le et le décalage.IEnumerable<T>

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

casperOne
la source
Merci pour votre réponse détaillée. J'espère que je ne suis pas impoli, mais cela n'a vraiment répondu à aucune de mes préoccupations avec ASP.NET MVC: qu'en est-il des entrées utilisateur (l'utilisation d'un classeur de modèles personnalisé suffirait-elle)? Et surtout, qu'en est-il de l' affichage de ces dates (dans le fuseau horaire local de l'utilisateur)? J'ai l'impression que la méthode d'extension va ajouter beaucoup de poids à mes vues, ce qui semble inutile.
TheCloudlessSky
1
@TheCloudlessSky: Voir les deux derniers paragraphes de ma réponse modifiée. Personnellement, je pense que les détails de l'endroit où effectuer les transformations sont mineurs; le problème majeur est la conversion et le stockage réels des données datetime (btw, je ne saurais trop insister sur l'utilisation de datetimeoffsetSQL Server et DateTimeOffset.NET ici, ils simplifient vraiment énormément les choses) que je pense que .NET ne gère pas correctement. du tout pour les raisons exposées ci-dessus. Si vous avez une date à New York qui a été entrée en 2003 et que vous souhaitez ensuite la traduire en une date à Los Angeles en 2011, .NET échoue dans ce cas.
casperOne
Le problème de ne pas utiliser de classeur de modèle (ou de toute couche avant l'action) est que chaque contrôleur devient très difficile à tester lorsque vous travaillez avec des dates (ils gagneraient tous une dépendance sur la conversion de date). Cela permettrait d'écrire les dates des tests unitaires en UTC. Mon application a des utilisateurs associés à un profil (pensez aux médecins / secrétaires associés à leur pratique). Le profil contient les informations de fuseau horaire. Par conséquent, il est assez facile d'obtenir le fuseau horaire du profil de l'utilisateur actuel et de le transformer pendant la liaison. Avez-vous d'autres arguments contre cela? Merci pour votre contribution!
TheCloudlessSky
Le cas contre cela est que vos tests ne refléteraient pas avec précision les cas de test. Votre entrée ne sera pas en UTC, donc vos cas de test ne doivent pas être conçus pour l'utiliser. Il devrait utiliser des dates du monde réel avec des emplacements et tout (bien que si vous utilisez le, DateTimeOffsetvous atténuerez beaucoup cela, OMI).
casperOne
Oui, mais cela signifie que chaque test qui traite des dates doit en tenir compte. Chaque action qui traite des datetimes sera toujours convertie en UTC. Pour moi, c'est un candidat idéal pour le classeur de modèles, puis pour tester le classeur de modèles .
TheCloudlessSky
5

C'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 du modèle de données. Une base de données peut stocker des données à l'heure du serveur local, mais il est du devoir de la couche de présentation de rendre la date / heure à l'aide du fuseau horaire de l'utilisateur local. Cela me semble le même problème que le I18N et le format des nombres pour différents pays. Dans votre cas, votre application doit détecter le Culturefuseau horaire et le fuseau horaire de l'utilisateur et modifier la vue en affichant différentes présentations de texte, de nombre et de date, mais les données stockées peuvent avoir le même format.

Massimo Zerbini
la source
Merci pour votre réponse. Ouais, c'est ce que j'ai essentiellement décrit, je me demande simplement comment cela pourrait être élégamment réalisé avec MVC. L'application stocke le fuseau horaire de l'utilisateur dans le cadre de la création de son compte (il choisit son fuseau horaire). Le problème vient encore une fois de savoir comment rendre les dates dans chaque vue sans (espérons-le) avoir à les remplir avec les méthodes pour lesquelles j'ai créé DateTimeExtensions.
TheCloudlessSky
Il existe de nombreuses façons de le faire, l'une consiste à utiliser des méthodes d'assistance comme vous l'avez suggéré, une autre peut-être plus complexe mais élégante est l'utilisation d'un filtre qui traite les demandes et les réponses et convertit le datetime. Une autre méthode consiste à développer un attribut d'affichage personnalisé pour annoter les champs de type DateTime de votre modèle de vue.
Massimo Zerbini le
Voilà le genre de choses que je recherchais. Si vous regardez mon OP, j'ai également mentionné l'utilisation d'AutoMapper pour effectuer un traitement avant d' accéder au résultat view / json mais après l'exécution de l'action.
TheCloudlessSky
1

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 des 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 qu'il fonctionne à la fois pour l'entrée et la sortie, je suggérerais d'étendre un contrôle ou même de créer le vôtre. De cette façon, vous pouvez intercepter à la fois l'entrée et les sorties et convertir le texte / la valeur selon vos besoins.

J'espère que ce lien vous poussera dans la bonne direction si vous souhaitez emprunter cette voie.

Quoi qu'il en soit, si vous voulez une solution élégante, ce sera un peu de travail. Du bon côté, une fois que vous l'avez fait, vous pouvez le conserver dans votre bibliothèque de code pour une utilisation future!

Dnatoli
la source
Je pense que les modèles et classeurs d'affichage / éditeur personnalisés sont la solution la plus élégante, car cela "fonctionnera" une fois implémenté. Aucun développeur aura besoin de savoir quelque chose de spécial pour obtenir les dates de travail à droite juste utiliser DisplayForet EditorForil fonctionne à chaque fois. +1
Kevin Stricker
17
cela ne rendrait-il pas l'heure du serveur d'applications et non celle du navigateur?
Alex
0

Il s'agit probablement d'un marteau pour casser une noix, mais vous pouvez injecter une couche entre les couches UI et Business qui convertit de manière transparente les datetimes en heure locale sur les graphiques d'objets renvoyés et en UTC sur les paramètres datetime 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 datetimes dans l'interface utilisateur ...

Geoffroy
la source