Come gestire elegantemente i fusi orari


140

Ho un sito Web ospitato in un fuso orario diverso rispetto agli utenti che utilizzano l'applicazione. Inoltre, gli utenti possono avere un fuso orario specifico. Mi chiedevo come gli altri utenti e applicazioni SO si avvicinano a questo? La parte più ovvia è che all'interno del DB, data / ora sono memorizzati in UTC. Quando si è sul server, tutte le date / orari devono essere trattati in UTC. Tuttavia, vedo tre problemi che sto cercando di superare:

  1. Ottenere l'ora corrente in UTC (risolto facilmente con DateTime.UtcNow).

  2. Estrarre data / ora dal database e visualizzarli per l'utente. Esistono potenzialmente molte chiamate per stampare le date su viste diverse. Stavo pensando a un livello tra la vista e i controller che potevano risolvere questo problema. O avere un metodo di estensione personalizzato attivato DateTime(vedi sotto). Il lato negativo principale è che in ogni posizione di utilizzo di un datetime in una vista, è necessario chiamare il metodo di estensione!

    Ciò aggiungerebbe inoltre difficoltà all'uso di qualcosa come JsonResult. Non si potrebbe più facilmente chiamare Json(myEnumerable), dovrebbe essere Json(myEnumerable.Select(transformAllDates)). Forse AutoMapper potrebbe aiutare in questa situazione?

  3. Ottenere input dall'utente (da locale a UTC). Ad esempio, POSTare un modulo con una data richiederebbe la conversione della data in UTC prima. La prima cosa che viene in mente è la creazione di un'abitudine ModelBinder.

Ecco le estensioni che ho pensato di utilizzare nelle viste:

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

Penserei che gestire i fusi orari sarebbe una cosa così comune considerando che molte applicazioni sono ora basate su cloud in cui l'ora locale del server potrebbe essere molto diversa dal fuso orario previsto.

Questo è stato elegantemente risolto prima? C'è qualcosa che mi manca? Idee e pensieri sono molto apprezzati.

EDIT: Per chiarire un po 'di confusione, ho pensato di aggiungere qualche dettaglio in più. Il problema in questo momento non è come memorizzare i tempi UTC nel db, è più sul processo di passaggio da UTC-> Local e Local-> UTC. Come sottolinea @Max Zerbini, è ovviamente intelligente mettere in vista il codice UTC-> Local, ma sta usando DateTimeExtensionsdavvero la risposta? Quando si riceve l'input dall'utente, ha senso accettare le date come l'ora locale dell'utente (poiché è quello che JS userebbe) e quindi utilizzare a ModelBinderper trasformarsi in UTC? Il fuso orario dell'utente è memorizzato nel DB ed è facilmente recuperabile.


3
Potresti prima leggere questo eccellente post ... Ora legale e best practice per il
fuso orario

@dodgy_coder - Questo è sempre stato un ottimo collegamento di risorse per i fusi orari. Tuttavia, non risolve davvero nessuno dei miei problemi (in particolare di MVC). Grazie comunque.
TheCloudlessSky,


Curioso quale soluzione hai scelto. Di fronte a una decisione simile io stesso. Buona domanda.
Sean,

@Sean - La soluzione finora non è stata elegante (motivo per cui devo ancora accettare una risposta). È stato un sacco di sovraccarico manuale di stampare date / orari e riconvertirli manualmente con a ModelBinder.
TheCloudlessSky

Risposte:


106

Non che si tratti di una raccomandazione, della sua più condivisione di un paradigma, ma il modo più aggressivo che ho visto di gestire le informazioni sul fuso orario in un'app Web (che non è esclusivo di ASP.NET MVC) era il seguente:

  • Tutti gli orari di data sul server sono UTC. Ciò significa usare, come hai detto DateTime.UtcNow,.

  • Cerca di fidarti del client che passa le date al server il meno possibile. Ad esempio, se hai bisogno di "ora", non creare una data sul client e quindi passarla al server. O crea una data nel tuo GET e passala al ViewModel o su POST do DateTime.UtcNow.

Finora, tariffa piuttosto standard, ma è qui che le cose diventano "interessanti".

  • Se devi accettare una data dal client, utilizza javascript per assicurarti che i dati che pubblichi sul server siano in UTC. Il cliente sa in quale fuso orario si trova, quindi può con ragionevole precisione convertire i tempi in UTC.

  • Durante il rendering delle viste, utilizzavano l' <time>elemento HTML5 , non eseguivano mai il rendering dei dati direttamente nel ViewModel. È stato implementato come HtmlHelperestensione, qualcosa di simile Html.Time(Model.when). Renderebbe <time datetime='[utctime]' data-date-format='[datetimeformat]'></time>.

    Quindi userebbero JavaScript per tradurre l'ora UTC nell'ora locale del client. Lo script trova tutti gli <time>elementi e usa la date-formatproprietà data per formattare la data e popolare il contenuto dell'elemento.

In questo modo non hanno mai dovuto tenere traccia, archiviare o gestire un fuso orario dei clienti. Al server non importava in quale fuso orario si trovasse il client, né doveva fare alcuna traduzione del fuso orario. Sputava semplicemente UTC e lasciava che il client lo convertisse in qualcosa di ragionevole. Che è facile dal browser, perché sa in che fuso orario si trova. Se il client ha cambiato il suo fuso orario, l'applicazione Web si aggiornerà automaticamente. L'unica cosa che hanno archiviato è stata la stringa di formato datetime per la locale dell'utente.

Non sto dicendo che sia stato l'approccio migliore, ma è stato un approccio diverso che non avevo mai visto prima. Forse ne trarrai alcune idee interessanti.


Grazie per la risposta. Quando si riceve l'input della data dall'utente (ad esempio la pianificazione di un appuntamento), non si presume che questo input sia nel loro fuso orario corrente? Cosa ne pensi del ModelBinder che fa questa trasformazione prima di entrare nell'azione (vedi i miei commenti a @casperOne. Inoltre, l' <time>idea è piuttosto interessante. L'unico svantaggio è che significa che devo interrogare l'intero DOM per questi elementi, che non è esattamente bello solo per trasformare le date (che dire di JS disabilitato?) Grazie ancora!
TheCloudlessSky

Di solito, sì, il presupposto è che sarebbe in un fuso orario locale. Ma hanno sottolineato che ogni volta che hanno raccolto un tempo da un utente, è stato trasformato in UTC usando javascript prima di essere inviato al server. La loro soluzione era molto pesante per JS e non si degradava molto bene quando JS era spento. Non ci ho pensato abbastanza per vedere se c'è qualcosa di intelligente in questo. Ma come ho detto, forse ti darà alcune idee.
J. Holmes,

2
Da allora ho lavorato su un altro progetto che aveva bisogno di date locali e questo era il metodo che ho usato. Sto usando moment.js per fare la formattazione di data / ora ... è dolce. Grazie!
TheCloudlessSky

Non esiste un modo semplice per definire in app.config / web.cofig dell'applicazione un fuso orario nell'ambito dell'applicazione e farlo trasformare tutti i valori di DateTime.Now in DateTime.UtcNow? (Voglio evitare situazioni in cui i programmatori della compagnia utilizzerebbero ancora DateTime. Adesso per errore)
Uri Abramson,

3
Uno svantaggio di questo approccio che posso vedere è quando stai usando il tempo per altre cose, creando pdf, e-mail e simili, non c'è un elemento temporale per quelli, quindi dovrai comunque convertirli manualmente. Altrimenti, soluzione molto accurata
shenku

15

Dopo diversi feedback, ecco la mia soluzione finale che ritengo pulita e semplice e copre i problemi dell'ora legale.

1 - Gestiamo la conversione a livello di modello. Quindi, nella classe Model, scriviamo:

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

2 - In un aiuto globale realizziamo la nostra funzione personalizzata "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 - Possiamo migliorarlo ulteriormente, salvando l'id del fuso orario in ciascun profilo utente in modo da poter recuperare dalla classe utente invece di utilizzare il "tempo standard cinese" costante:

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

4 - Qui possiamo ottenere l'elenco dei fusi orari da mostrare all'utente per selezionare da un menu a discesa:

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

Quindi, ora alle 09:25 in Cina, sito Web ospitato negli Stati Uniti, data salvata in UTC nel database, ecco il risultato finale:

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)

MODIFICARE

Grazie a Matt Johnson per aver sottolineato le parti deboli della soluzione originale, e mi dispiace per aver eliminato i post originali, ma ho avuto problemi a ottenere il giusto formato di visualizzazione del codice ... si è scoperto che l'editor ha problemi a mescolare "proiettili" con "pre-codice", quindi rimosso i bulli ed era ok.


Sembra fattibile se qualcuno non ha l'architettura di livello n o le librerie Web sono condivise. Come possiamo condividere l'id del fuso orario con il livello dati.
Vikash Kumar,

9

Nella sezione eventi su sf4answers , gli utenti inseriscono un indirizzo per un evento, nonché una data di inizio e una data di fine opzionale. Questi tempi sono tradotti in un datetimeoffsetserver in SQL che tiene conto dell'offset da UTC.

Questo è lo stesso problema che stai affrontando (anche se stai adottando un approccio diverso, in quanto stai usando DateTime.UtcNow); hai una posizione e devi tradurre un orario da un fuso orario a un altro.

Ci sono due cose principali che ho fatto che hanno funzionato per me. Innanzitutto, usa la DateTimeOffsetstruttura , sempre. Rappresenta l'offset da UTC e se riesci a ottenere tali informazioni dal tuo cliente, ti semplifica la vita.

In secondo luogo, quando si eseguono le traduzioni, supponendo che si conosca la posizione / il fuso orario in cui si trova il client, è possibile utilizzare il database del fuso orario delle informazioni pubbliche per tradurre un orario da UTC a un altro fuso orario (o triangolare, se lo si desidera, tra due Fusi orari). La cosa grandiosa del database tz (a volte indicato come il database Olson ) è che tiene conto dei cambiamenti nei fusi orari nel corso della storia; ottenere un offset è una funzione della data in cui si desidera ottenere l'offset (basta guardare l' Energy Policy Act del 2005 che ha cambiato le date in cui l'ora legale entra in vigore negli Stati Uniti ).

Con il database in mano, è possibile utilizzare l' API .NET ZoneInfo (database tz / database Olson) . Nota che non esiste una distribuzione binaria, dovrai scaricare l' ultima versione e compilarla tu stesso.

Al momento in cui scrivo, al momento analizza tutti i file nell'ultima distribuzione di dati (in realtà l'ho eseguito sul file ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz il 25 settembre, 2011; a marzo 2017, lo riceveresti tramite https://iana.org/time-zones o da ftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gz ).

Quindi su sf4answers, dopo aver ottenuto l'indirizzo, viene geocodificato in una combinazione latitudine / longitudine e quindi inviato a un servizio Web di terze parti per ottenere un fuso orario che corrisponde a una voce nel database tz. Da lì, i tempi di inizio e fine vengono convertiti in DateTimeOffsetistanze con l'offset UTC corretto e quindi memorizzati nel database.

Per quanto riguarda la gestione su SO e siti Web, dipende dal pubblico e da ciò che si sta tentando di visualizzare. Se noti, la maggior parte dei siti Web social (e SO, e la sezione degli eventi su sf4answers) visualizzano gli eventi in tempo relativo o, se viene utilizzato un valore assoluto, di solito è UTC.

Tuttavia, se il tuo pubblico si aspetta l'ora locale, utilizzare DateTimeOffsetinsieme a un metodo di estensione che impiega il fuso orario per la conversione andrebbe bene; il tipo di dati SQL datetimeoffsetsi tradurrebbe in .NET DateTimeOffsetche è quindi possibile ottenere il tempo universale per l'utilizzo del GetUniversalTimemetodo . Da lì, usi semplicemente i metodi della ZoneInfoclasse per convertire da UTC all'ora locale (dovrai fare un po 'di lavoro per farlo entrare in a DateTimeOffset, ma è abbastanza semplice da fare).

Dove fare la trasformazione? È un costo che dovrai pagare da qualche parte e non esiste un modo "migliore". Opterei per la vista, con l'offset del fuso orario come parte del modello di vista presentato alla vista. In questo modo, se i requisiti per la vista cambiano, non è necessario modificare il modello di vista per adattarlo alla modifica. Dovresti JsonResultsemplicemente contenere un modello con e l'offset.IEnumerable<T>

Sul lato input, usando un raccoglitore modello? Direi assolutamente no. Non puoi garantire che tutte le date (ora o in futuro) dovranno essere trasformate in questo modo, dovrebbe essere una funzione esplicita del tuo controller per eseguire questa azione. Ancora una volta, se i requisiti cambiano, non è necessario modificare una o più ModelBinderistanze per adattare la logica aziendale; ed è una logica aziendale, il che significa che dovrebbe essere nel controller.


Grazie per la tua risposta dettagliata. Spero di non sembrare scortese, ma questo non ha davvero risposto a nessuna delle mie preoccupazioni con ASP.NET MVC: che dire dell'input dell'utente (sarebbe sufficiente usare un raccoglitore di modelli personalizzato)? E, soprattutto, che ne dici di visualizzare queste date (nel fuso orario locale dell'utente)? Sento che il metodo di estensione aggiungerà molto peso ai miei punti di vista che sembra superfluo.
TheCloudlessSky,

1
@TheCloudlessSky: vedi gli ultimi due paragrafi della mia risposta modificata. Personalmente, penso che i dettagli su dove fare le trasformazioni siano minori; il problema principale è l'effettiva conversione e archiviazione dei dati del datetime (a proposito, non posso enfatizzare abbastanza l'uso di datetimeoffsetin SQL Server e DateTimeOffsetin .NET qui, semplificano enormemente le cose) che credo che .NET non gestisca adeguatamente affatto per i motivi indicati sopra. Se hai una data a New York che è stata inserita nel 2003, e quindi vuoi che sia tradotta in una data a Los Angeles nel 2011, .NET in questo caso non riesce.
casperIl

Il problema di non utilizzare un raccoglitore di modelli (o qualsiasi livello prima dell'azione) è che ogni controller diventa molto difficile da testare quando si lavora con le date (otterrebbero tutti una dipendenza dalla conversione della data). Ciò consentirebbe di scrivere le date dei test unitari in UTC. La mia applicazione ha utenti associati a un profilo (pensa ai dottori / segretari associati alla loro pratica). Il profilo contiene le informazioni sul fuso orario. Pertanto, è abbastanza facile ottenere il fuso orario del profilo dell'utente corrente e trasformarlo durante l'associazione. Hai altri argomenti contrari? Grazie per il tuo contributo!
TheCloudlessSky,

Il caso contrario è che i tuoi test non riflettono accuratamente i casi di test. Il tuo input non sarà in UTC, quindi i casi di test non dovrebbero essere predisposti per usarlo. Dovrebbe usare le date del mondo reale con le posizioni e tutto (anche se se lo usi lo DateTimeOffsetmitigherai molto, IMO).
casperUn

Sì, ma questo significa che ogni test che si occupa delle date deve tener conto di ciò. Ogni azione che si occupa di orari dei dati verrà sempre convertita in UTC. Per me questo è un candidato principale per il raccoglitore modello e quindi testare il raccoglitore modello .
TheCloudlessSky,

5

Questa è solo la mia opinione, penso che l'applicazione MVC dovrebbe separare bene il problema di presentazione dei dati dalla gestione del modello di dati. Un database può archiviare i dati nell'ora del server locale, ma è dovere del livello di presentazione rendere il datetime usando il fuso orario dell'utente locale. Questo mi sembra lo stesso problema di I18N e formato numerico per diversi paesi. Nel tuo caso, l'applicazione dovrebbe rilevare il Culturefuso orario e l'utente e modificare la visualizzazione che mostra testo, numero e presentazione dei dati diversi, ma i dati memorizzati possono avere lo stesso formato.


Grazie per la risposta. Sì, questo è quello che ho sostanzialmente descritto, mi chiedo solo come questo possa essere elegantemente realizzato con MVC. L'app memorizza il fuso orario dell'utente durante la creazione dell'account (seleziona il fuso orario). Il problema si presenta di nuovo con come rendere le date in ogni vista senza (si spera) doverle sporcare con i metodi per cui ho creato DateTimeExtensions.
TheCloudlessSky,

Ci sono molti modi per farlo, uno è usare i metodi di supporto come hai suggerito, un altro forse più complesso ma elegante è l'uso di un filtro che elabora le richieste e le risposte e converte il datetime. Un altro modo è quello di sviluppare un attributo Display personalizzato per annotare i campi di tipo DateTime del modello di visualizzazione.
Massimo Zerbini,

Questi sono i tipi di cose che stavo cercando. Se dai un'occhiata al mio OP, ho anche menzionato l'uso di AutoMapper per eseguire alcune elaborazioni prima di passare al risultato view / json ma dopo che l'azione è stata eseguita.
TheCloudlessSky,

1

Per l'output, creare un modello di visualizzazione / editor come questo

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

È possibile associarli in base agli attributi del modello se si desidera che solo determinati modelli utilizzino tali modelli.

Vedi qui e qui per maggiori dettagli sulla creazione di modelli di editor personalizzati.

In alternativa, poiché vuoi che funzioni sia per l'input che per l'output, suggerirei di estendere un controllo o persino di crearne uno tuo. In questo modo è possibile intercettare sia l'input che l'output e convertire il testo / valore secondo necessità.

Si spera che questo link ti spinga nella giusta direzione se vuoi percorrere quella strada.

Ad ogni modo, se vuoi una soluzione elegante, sarà un po 'di lavoro. Il lato positivo, una volta che lo hai fatto una volta, puoi tenerlo nella tua libreria di codici per un uso futuro!


Penso che i modelli di visualizzazione / editor personalizzati e il raccoglitore siano la soluzione più elegante, perché "funzioneranno" una volta implementati. Nessuno sviluppatore avrà bisogno di sapere nulla di speciale per ottenere le date di lavoro giusto basta usare DisplayFored EditorFore funzionerà ogni volta. +1
Kevin Stricker,

17
questo non renderebbe l'ora del server delle applicazioni e non l'ora del browser?
Alex,

0

Questo è probabilmente un martello per rompere un dado, ma è possibile iniettare un livello tra l'interfaccia utente e i livelli aziendali che converte in modo trasparente i tempi dei dati nell'ora locale sui grafici degli oggetti restituiti e in UTC sui parametri di data e ora di input.

Immagino che ciò possa essere ottenuto usando PostSharp o qualche inversione del contenitore di controllo.

Personalmente, vorrei semplicemente convertire esplicitamente i tuoi dati nell'interfaccia utente ...

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.