Cara elegan menangani zona waktu

140

Saya memiliki situs web yang dihosting di zona waktu berbeda dari pengguna yang menggunakan aplikasi. Selain itu, pengguna dapat memiliki zona waktu tertentu. Saya bertanya-tanya bagaimana pengguna SO lainnya dan aplikasi mendekati ini? Bagian yang paling jelas adalah bahwa di dalam DB, tanggal / waktu disimpan dalam UTC. Ketika di server, semua tanggal / waktu harus ditangani dalam UTC. Namun, saya melihat tiga masalah yang saya coba atasi:

  1. Mendapatkan waktu saat ini di UTC (diselesaikan dengan mudah dengan DateTime.UtcNow).

  2. Menarik tanggal / waktu dari database dan menampilkannya kepada pengguna. Ada banyak kemungkinan panggilan untuk mencetak tanggal pada tampilan yang berbeda. Saya sedang memikirkan beberapa lapisan di antara tampilan dan pengontrol yang dapat menyelesaikan masalah ini. Atau memiliki metode ekstensi khusus pada DateTime(lihat di bawah). Sisi buruk utama adalah bahwa di setiap lokasi menggunakan datetime dalam tampilan, metode ekstensi harus dipanggil!

    Ini juga akan menambah kesulitan untuk menggunakan sesuatu seperti JsonResult. Anda tidak bisa lagi dengan mudah menelepon Json(myEnumerable), itu harus terjadi Json(myEnumerable.Select(transformAllDates)). Mungkin AutoMapper dapat membantu dalam situasi ini?

  3. Mendapatkan input dari pengguna (Lokal ke UTC). Sebagai contoh, POSTing formulir dengan tanggal akan memerlukan konversi tanggal ke UTC sebelumnya. Hal pertama yang terlintas dalam pikiran adalah menciptakan kebiasaan ModelBinder.

Berikut ini ekstensi yang saya pikir akan digunakan dalam tampilan:

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

Saya akan berpikir bahwa berurusan dengan zona waktu akan menjadi hal yang umum mengingat banyak aplikasi sekarang berbasis cloud di mana waktu lokal server bisa jauh berbeda dari zona waktu yang diharapkan.

Apakah ini telah diselesaikan dengan elegan sebelumnya? Apakah ada sesuatu yang saya lewatkan? Gagasan dan pemikiran sangat dihargai.

EDIT: Untuk menghapus beberapa kebingungan saya pikir menambahkan beberapa detail lagi. Masalahnya sekarang bukan bagaimana menyimpan waktu UTC di db, ini lebih tentang proses pergi dari UTC-> Lokal dan Lokal-> UTC. Seperti yang ditunjukkan oleh @Max Zerbini, jelas pintar untuk menempatkan UTC-> kode lokal dalam tampilan, tetapi apakah DateTimeExtensionsbenar-benar menggunakan jawabannya? Ketika mendapatkan input dari pengguna, apakah masuk akal untuk menerima tanggal sebagai waktu lokal pengguna (karena itulah yang akan digunakan JS) dan kemudian menggunakan a ModelBinderuntuk mengubah ke UTC? Zona waktu pengguna disimpan dalam DB dan mudah diambil.

TheCloudlessSky
sumber
3
Pertama-tama Anda mungkin harus membaca posting yang luar biasa ini ... Waktu musim panas dan praktik terbaik Timezone
dodgy_coder
@dodgy_coder - Itu selalu menjadi sumber daya tautan yang bagus untuk zona waktu. Namun, itu tidak benar-benar menyelesaikan masalah saya (khususnya yang berkaitan dengan MVC). Terimakasih Meskipun.
TheCloudlessSky
code.google.com/p/noda-time mungkin berguna
Arnis Lapsa
Ingin tahu solusi mana yang telah Anda pilih. Menghadapi keputusan serupa sendiri. Pertanyaan bagus.
Sean
@Sean - Solusinya sejauh ini belum elegan (itulah sebabnya saya belum menerima jawaban). Sudah banyak overhead manual tanggal cetak / kali dan secara manual mengubahnya kembali dengan a ModelBinder.
TheCloudlessSky

Jawaban:

106

Bukannya ini adalah rekomendasi, ini lebih merupakan pembagian paradigma, tetapi cara paling agresif yang pernah saya lihat dalam menangani informasi zona waktu di aplikasi web (yang tidak eksklusif untuk ASP.NET MVC) adalah sebagai berikut:

  • Semua waktu tanggal di server adalah UTC. Itu berarti menggunakan, seperti yang Anda katakan DateTime.UtcNow,.

  • Cobalah untuk mempercayai klien melewati tanggal ke server sesedikit mungkin. Misalnya, jika Anda perlu "sekarang", jangan membuat tanggal pada klien dan kemudian meneruskannya ke server. Buat tanggal di GET Anda dan berikan ke ViewModel atau POST do DateTime.UtcNow.

Sejauh ini, ongkosnya lumayan standar, tapi di sinilah segalanya menjadi 'menarik'.

  • Jika Anda harus menerima tanggal dari klien, maka gunakan javascript untuk memastikan data yang Anda posting ke server ada di UTC. Klien tahu zona waktu apa yang digunakan, sehingga dapat dengan akurat mengubah waktu menjadi UTC.

  • Saat merender tampilan, mereka menggunakan <time>elemen HTML5 , mereka tidak akan merender datetimes secara langsung di ViewModel. Diimplementasikan sebagai HtmlHelperekstensi, sesuatu seperti Html.Time(Model.when). Itu akan membuat <time datetime='[utctime]' data-date-format='[datetimeformat]'></time>.

    Kemudian mereka akan menggunakan javascript untuk menerjemahkan waktu UTC ke waktu lokal klien. Script akan menemukan semua <time>elemen dan menggunakan date-formatproperti data untuk memformat tanggal dan mengisi konten elemen.

Dengan cara ini mereka tidak pernah harus melacak, menyimpan, atau mengelola zona waktu klien. Server tidak peduli zona waktu apa yang digunakan klien, juga tidak perlu melakukan terjemahan zona waktu apa pun. Itu hanya memuntahkan UTC dan membiarkan klien mengubahnya menjadi sesuatu yang masuk akal. Yang mana mudah dari peramban, karena tahu zona waktu apa yang digunakan. Jika klien mengubah zona waktunya, aplikasi web akan secara otomatis memperbarui sendiri. Satu-satunya hal yang mereka simpan adalah string format datetime untuk lokal pengguna.

Saya tidak mengatakan itu adalah pendekatan terbaik, tapi itu pendekatan yang berbeda yang belum pernah saya lihat sebelumnya. Mungkin Anda akan mendapatkan beberapa ide menarik darinya.

J. Holmes
sumber
Terimakasih atas tanggapan Anda. Saat mendapatkan input tanggal dari pengguna (misalnya menjadwalkan janji temu), bukankah input ini dianggap berada di zona waktu saat ini? Apa yang Anda pikirkan tentang ModelBinder melakukan transformasi ini sebelum memasukkan tindakan (lihat komentar saya ke @casperOne. Juga, <time>idenya cukup keren. Satu-satunya kelemahan adalah bahwa itu berarti bahwa saya perlu meminta seluruh DOM untuk elemen-elemen ini, yang tidak persis baik hanya untuk mengubah tanggal (bagaimana dengan JS dinonaktifkan?) Terima kasih lagi!
TheCloudlessSky
Biasanya, ya asumsinya adalah bahwa itu akan berada di zona waktu lokal. Tetapi mereka memastikan bahwa setiap kali mereka mengumpulkan waktu dari pengguna, itu diubah menjadi UTC menggunakan javascript sebelum dikirim ke server. Solusi mereka sangat berat untuk JS, dan tidak menurun dengan sangat baik ketika JS tidak aktif. Saya belum cukup memikirkannya untuk melihat apakah ada yang pintar di sekitar itu. Tapi seperti yang saya katakan mungkin itu akan memberi Anda beberapa ide.
J. Holmes
2
Sejak itu saya bekerja di proyek lain yang membutuhkan tanggal lokal dan ini adalah metode yang saya gunakan. Saya menggunakan moment.js untuk melakukan format tanggal / waktu ... ini manis. Terima kasih!
TheCloudlessSky
Apakah tidak ada cara sederhana untuk mendefinisikan di app.config / web.cofig aplikasi zona waktu Application-scope dan menjadikannya mengubah semua DateTime. Sekarang nilai menjadi DateTime.UtcNow? (Saya ingin menghindari situasi di mana programmer di perusahaan masih akan menggunakan DateTime. Sekarang karena kesalahan)
Uri Abramson
3
Salah satu kelemahan dari pendekatan ini yang bisa saya lihat adalah ketika Anda menggunakan waktu untuk hal-hal lain, membuat pdf, email dan semacamnya, tidak ada elemen waktu untuk itu sehingga Anda masih harus mengonversinya secara manual. Kalau tidak, solusi yang sangat rapi
shenku
15

Setelah beberapa umpan balik, inilah solusi terakhir saya yang menurut saya bersih dan sederhana dan mencakup masalah penghematan siang hari.

1 - Kami menangani konversi di tingkat model. Jadi, di kelas Model, kita menulis:

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

2 - Dalam penolong global, kami membuat fungsi kustom kami "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 - Kami dapat meningkatkan ini lebih lanjut, dengan menyimpan id zona waktu di setiap profil Pengguna sehingga kami dapat mengambil dari kelas pengguna alih-alih menggunakan "Waktu Standar China" yang konstan:

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

4 - Di sini kita bisa mendapatkan daftar zona waktu untuk ditampilkan kepada pengguna untuk dipilih dari dropdownbox:

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

Jadi, sekarang pada jam 9:25 di Cina, Situs web di-host di AS, tanggal disimpan dalam UTC di basis data, inilah hasil akhirnya:

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)

EDIT

Terima kasih kepada Matt Johnson untuk menunjukkan bagian lemah dari solusi asli, dan maaf karena menghapus posting asli, tetapi mendapat masalah dengan format tampilan kode yang benar ... ternyata editor memiliki masalah mencampur "peluru" dengan "kode pra", jadi saya dihapus bulles dan itu ok.

Nestor
sumber
Tampaknya bisa diterapkan jika seseorang tidak memiliki arsitektur n-tier atau perpustakaan Web dibagi. Bagaimana kami dapat berbagi id zona waktu ke lapisan data.
Vikash Kumar
9

Di bagian acara di sf4answer , pengguna memasukkan alamat untuk suatu acara, serta tanggal mulai dan tanggal akhir opsional. Waktu-waktu ini diterjemahkan ke datetimeoffsetdalam server SQL yang memperhitungkan offset dari UTC.

Ini adalah masalah yang sama yang Anda hadapi (meskipun Anda mengambil pendekatan yang berbeda untuk itu, dalam hal yang Anda gunakan DateTime.UtcNow); Anda memiliki lokasi dan Anda perlu menerjemahkan waktu dari satu zona waktu ke yang lain.

Ada dua hal utama yang saya lakukan yang bekerja untuk saya. Pertama, gunakan DateTimeOffsetstruktur , selalu. Itu menyumbang offset dari UTC dan jika Anda bisa mendapatkan informasi itu dari klien Anda, itu membuat hidup Anda sedikit lebih mudah.

Kedua, saat melakukan terjemahan, dengan asumsi Anda tahu lokasi / zona waktu tempat klien berada, Anda dapat menggunakan basis data zona waktu info publik untuk menerjemahkan waktu dari UTC ke zona waktu lain (atau melakukan triangulasi, jika Anda mau, antara dua zona waktu). Hal yang hebat tentang basis data tz (kadang-kadang disebut sebagai basis data Olson ) adalah bahwa ia bertanggung jawab atas perubahan zona waktu sepanjang sejarah; mendapatkan offset adalah fungsi dari tanggal di mana Anda ingin mendapatkan offset tersebut (lihat saja Undang-Undang Kebijakan Energi 2005 yang mengubah tanggal ketika waktu penghematan siang hari berlaku di AS ).

Dengan database di tangan, Anda dapat menggunakan ZoneInfo (tz database / Olson database) .NET API . Perhatikan bahwa tidak ada distribusi biner, Anda harus mengunduh versi terbaru dan mengompilasinya sendiri.

Pada saat penulisan ini, saat ini mem-parsing semua file dalam distribusi data terbaru (saya benar-benar menjalankannya terhadap file ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz pada 25 September, 2011; pada Maret 2017, Anda akan mendapatkannya melalui https://iana.org/time-zones atau dari ftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gz ).

Jadi pada sf4answers, setelah mendapatkan alamat, geocoded menjadi kombinasi lintang / bujur dan kemudian dikirim ke layanan web pihak ketiga untuk mendapatkan zona waktu yang sesuai dengan entri dalam basis data tz. Dari sana, waktu mulai dan berakhir dikonversi menjadi DateTimeOffsetinstance dengan offset UTC yang tepat dan kemudian disimpan dalam database.

Adapun untuk menghadapinya di SO dan situs web, itu tergantung pada audiens dan apa yang Anda coba tampilkan. Jika Anda perhatikan, sebagian besar situs web sosial (dan SO, dan bagian acara di sf4answer) menampilkan acara dalam waktu relatif , atau, jika nilai absolut digunakan, biasanya UTC.

Namun, jika audiens Anda mengharapkan waktu lokal, maka menggunakan DateTimeOffsetbersama dengan metode ekstensi yang mengambil zona waktu untuk dikonversi akan baik-baik saja; tipe data SQL datetimeoffsetakan menerjemahkan ke. NET DateTimeOffsetyang kemudian Anda bisa mendapatkan waktu universal untuk menggunakan GetUniversalTimemetode ini . Dari sana, Anda cukup menggunakan metode di ZoneInfokelas untuk mengkonversi dari UTC ke waktu lokal (Anda harus melakukan sedikit pekerjaan untuk membuatnya menjadi DateTimeOffset, tetapi cukup sederhana untuk dilakukan).

Di mana harus melakukan transformasi? Itu adalah biaya yang harus Anda bayar di suatu tempat , dan tidak ada cara "terbaik". Saya akan memilih tampilan, dengan zona waktu diimbangi sebagai bagian dari model tampilan yang disajikan kepada tampilan. Dengan begitu, jika persyaratan untuk tampilan berubah, Anda tidak perlu mengubah model tampilan Anda untuk mengakomodasi perubahan. Anda JsonResulthanya akan berisi model dengan dan offset.IEnumerable<T>

Di sisi input, menggunakan pengikat model? Saya akan mengatakan tidak mungkin. Anda tidak dapat menjamin bahwa semua tanggal (sekarang atau di masa depan) harus diubah dengan cara ini, itu harus menjadi fungsi eksplisit dari pengontrol Anda untuk melakukan tindakan ini. Sekali lagi, jika persyaratan berubah, Anda tidak perlu mengubah satu atau banyak ModelBindercontoh untuk menyesuaikan logika bisnis Anda; dan itu adalah logika bisnis, yang berarti harus ada di controller.

casperOne
sumber
Terima kasih atas tanggapan terperinci Anda. Saya harap saya tidak dianggap kasar, tetapi ini tidak benar-benar menjawab kekhawatiran saya dengan ASP.NET MVC: Bagaimana dengan input pengguna (akan menggunakan binder model kustom sudah cukup)? Dan yang paling penting, bagaimana dengan menampilkan tanggal ini (di zona waktu lokal pengguna)? Saya merasa metode ekstensi akan menambah banyak bobot pada pandangan saya yang tampaknya tidak perlu.
TheCloudlessSky
1
@TheCloudlessSky: Lihat dua paragraf terakhir dari respons saya yang diedit. Secara pribadi, saya pikir rincian tempat untuk melakukan transformasi kecil; masalah utama adalah konversi aktual dan penyimpanan data datetime (btw, saya tidak bisa cukup menekankan penggunaan datetimeoffsetdi SQL Server dan DateTimeOffset. NET di sini, mereka benar-benar menyederhanakan banyak hal) yang saya percaya. NET tidak menangani secara memadai sama sekali untuk alasan yang diuraikan di atas. Jika Anda memiliki tanggal di NYC yang dimasukkan pada tahun 2003, dan kemudian Anda menginginkannya diterjemahkan ke tanggal di LA pada tahun 2011, .NET gagal dalam hal ini.
casperOne
Masalah dengan tidak menggunakan pengikat model (atau lapisan apa pun sebelum tindakan) adalah bahwa setiap pengontrol menjadi sangat sulit untuk diuji ketika bekerja dengan tanggal (mereka semua akan mendapatkan ketergantungan pada konversi tanggal). Ini akan memungkinkan tanggal pengujian unit ditulis dalam UTC. Aplikasi saya memiliki pengguna yang dikaitkan dengan profil (pikirkan dokter / sekretaris yang terkait dengan praktik mereka). Profil ini menyimpan informasi zona waktu. Karenanya, cukup mudah untuk mendapatkan zona waktu profil pengguna saat ini dan mentransformasikannya selama pengikatan. Apakah Anda memiliki argumen lain yang menentang ini? Terima kasih atas masukan Anda!
TheCloudlessSky
Kasus terhadap itu adalah bahwa tes Anda tidak akan secara akurat mencerminkan kasus uji. Masukan Anda tidak akan berada di UTC, jadi kotak uji Anda tidak boleh dibuat untuk menggunakannya. Seharusnya menggunakan tanggal dunia nyata dengan lokasi dan semua (walaupun jika Anda menggunakan DateTimeOffsetAnda akan mengurangi ini, IMO).
casperOne
Ya - tetapi ini berarti bahwa setiap tes yang berhubungan dengan tanggal harus memperhitungkan ini. Setiap tindakan yang berhubungan dengan data akan selalu dikonversi ke UTC. Bagi saya ini adalah kandidat utama untuk pengikat model dan kemudian menguji pengikat model .
TheCloudlessSky
5

Ini hanya pendapat saya, saya pikir aplikasi MVC harus memisahkan masalah penyajian data dengan baik dari manajemen model data. Basis data dapat menyimpan data dalam waktu server lokal tetapi merupakan tugas lapisan presentasi untuk membuat data menggunakan zona waktu pengguna lokal. Bagi saya ini masalah yang sama dengan I18N dan format angka untuk berbagai negara. Dalam kasus Anda, aplikasi Anda harus mendeteksi Culturezona waktu pengguna dan mengubah tampilan yang menampilkan presentasi teks, angka, dan datime yang berbeda, tetapi data yang disimpan dapat memiliki format yang sama.

Massimo Zerbini
sumber
Terimakasih atas tanggapan Anda. Ya inilah yang pada dasarnya saya jelaskan, saya hanya ingin tahu bagaimana ini bisa dicapai secara elegan dengan MVC. Aplikasi menyimpan zona waktu pengguna sebagai bagian dari pembuatan akun mereka (mereka memilih zona waktu mereka). Masalahnya lagi datang dengan cara membuat tanggal di setiap tampilan tanpa (mudah-mudahan) harus membuangnya dengan metode yang saya buat DateTimeExtensions.
TheCloudlessSky
Ada banyak cara untuk melakukan ini, satu adalah dengan menggunakan metode pembantu seperti yang Anda sarankan, yang lain mungkin lebih kompleks tetapi elegan adalah penggunaan filter yang memproses permintaan dan tanggapan dan mengubah datetime. Cara lain adalah dengan mengembangkan atribut Tampilan kustom untuk membubuhi keterangan bidang tipe DateTime model tampilan Anda.
Massimo Zerbini
Itulah hal-hal yang saya cari. Jika Anda melihat OP saya, saya juga menyebutkan menggunakan AutoMapper untuk melakukan pemrosesan sebelum pergi ke tampilan / hasil json tetapi setelah tindakan telah dieksekusi.
TheCloudlessSky
1

Untuk hasil, buat template tampilan / editor seperti ini

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

Anda dapat mengikatnya berdasarkan atribut pada model Anda jika Anda hanya menginginkan model tertentu untuk menggunakan templat tersebut.

Lihat di sini dan di sini untuk rincian lebih lanjut tentang cara membuat template editor khusus.

Atau, karena Anda ingin itu berfungsi untuk input dan output, saya sarankan memperluas kontrol atau bahkan membuat Anda sendiri. Dengan begitu Anda dapat mencegat input dan output dan mengonversi teks / nilai sesuai kebutuhan.

Tautan ini diharapkan akan mendorong Anda ke arah yang benar jika Anda ingin menyusuri jalan itu.

Either way, jika Anda menginginkan solusi yang elegan, itu akan menjadi sedikit pekerjaan. Sisi baiknya, begitu Anda selesai melakukannya, Anda bisa menyimpannya di pustaka kode untuk digunakan di lain waktu!

dnatoli
sumber
Saya pikir template tampilan / editor kustom dan pengikat adalah solusi paling elegan, karena itu akan "hanya berfungsi" setelah diimplementasikan. Tidak ada pengembang yang perlu mengetahui sesuatu yang istimewa untuk mendapatkan tanggal yang berfungsi hanya dengan menggunakan DisplayFordan EditorFordan itu akan bekerja setiap saat. +1
Kevin Stricker
17
bukankah ini membuat waktu server aplikasi dan bukan waktu browser?
Alex
0

Ini mungkin palu godam untuk memecahkan kacang, tetapi Anda bisa menyuntikkan lapisan antara UI dan lapisan Bisnis yang secara transparan mengubah waktu data ke waktu lokal pada grafik objek yang dikembalikan, dan ke UTC pada parameter datetime input.

Saya membayangkan ini bisa dicapai dengan menggunakan PostSharp atau inversi wadah kontrol.

Secara pribadi, saya hanya akan secara eksplisit mengkonversi datetimes Anda di UI ...

geoffrey
sumber