Jak elegancko radzić sobie ze strefami czasowymi

140

Mam witrynę internetową, która jest hostowana w innej strefie czasowej niż użytkownicy korzystający z aplikacji. Oprócz tego użytkownicy mogą mieć określoną strefę czasową. Zastanawiałem się, jak podchodzą do tego inni użytkownicy SO i aplikacje? Najbardziej oczywistą częścią jest to, że w DB data / czas są przechowywane w UTC. Na serwerze wszystkie daty / godziny powinny być obsługiwane w czasie UTC. Widzę jednak trzy problemy, które próbuję rozwiązać:

  1. Pobieranie aktualnego czasu w UTC (łatwe rozwiązanie za pomocą DateTime.UtcNow).

  2. Pobieranie daty / godzin z bazy danych i wyświetlanie ich użytkownikowi. Potencjalnie jest wiele wezwań do drukowania dat w różnych widokach. Myślałem o jakiejś warstwie między widokiem a kontrolerami, która mogłaby rozwiązać ten problem. Lub mając włączoną niestandardową metodę rozszerzenia DateTime(patrz poniżej). Główną wadą jest to, że w każdym miejscu korzystania z daty i godziny w widoku należy wywołać metodę rozszerzenia!

    To również utrudniłoby używanie czegoś takiego jak JsonResult. Nie można już było łatwo zadzwonić Json(myEnumerable), musiałoby być Json(myEnumerable.Select(transformAllDates)). Może AutoMapper mógłby pomóc w tej sytuacji?

  3. Pobieranie danych od użytkownika (lokalnie do UTC). Na przykład POST przesłanie formularza z datą wymagałoby konwersji daty na UTC wcześniej. Pierwszą rzeczą, która przychodzi na myśl, jest stworzenie niestandardowego ModelBinder.

Oto rozszerzenia, których użyłem w widokach:

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

Myślę, że zajmowanie się strefami czasowymi byłoby tak powszechne, biorąc pod uwagę, że wiele aplikacji jest teraz opartych na chmurze, gdzie lokalny czas serwera może być znacznie inny niż oczekiwana strefa czasowa.

Czy zostało to wcześniej elegancko rozwiązane? Czy jest coś, czego mi brakuje? Bardzo cenione są pomysły i przemyślenia.

EDYCJA: Aby usunąć zamieszanie, pomyślałem, że dodaj więcej szczegółów. Kwestia teraz nie ma jak do przechowywania UTC razy w db, to więcej o procesie dzieje z UTC-> Lokalne> szczeblu lokalnym i UTC. Jak wskazuje @Max Zerbini, oczywiście mądrze jest umieścić kod UTC-> lokalny w widoku, ale czy DateTimeExtensionsnaprawdę używa się odpowiedzi? Czy podczas uzyskiwania danych wejściowych od użytkownika sensowne jest akceptowanie dat jako czasu lokalnego użytkownika (ponieważ tego właśnie używałby JS), a następnie używanie ModelBinderdo przekształcenia na UTC? Strefa czasowa użytkownika jest przechowywana w bazie danych i można ją łatwo odzyskać.

TheCloudlessSky
źródło
3
Być może najpierw przeczytasz ten doskonały post ... Najlepsze praktyki dotyczące czasu letniego i strefy czasowej
dodgy_coder
@dodgy_coder - To zawsze było świetne łącze do zasobów dla stref czasowych. Jednak tak naprawdę nie rozwiązuje żadnego z moich problemów (szczególnie dotyczących MVC). W każdym razie dzięki.
TheCloudlessSky
code.google.com/p/noda-time może być przydatny
Arnis Lapsa
Ciekawe, które rozwiązanie wybrałeś. Sam mam przed sobą podobną decyzję. Dobre pytanie.
Sean
@Sean - Jak dotąd rozwiązanie nie było eleganckie (dlatego jeszcze nie zaakceptowałem odpowiedzi). Było dużo ręcznego narzucania dat / godzin i ręcznego konwertowania ich z powrotem za pomocą pliku ModelBinder.
TheCloudlessSky

Odpowiedzi:

106

Nie chodzi o to, że jest to zalecenie, bardziej rozpowszechnianie paradygmatu, ale najbardziej agresywny sposób obsługi informacji o strefie czasowej w aplikacji internetowej (który nie dotyczy wyłącznie ASP.NET MVC), jaki widziałem, był następujący:

  • Wszystkie daty na serwerze to UTC. Oznacza użyciu, tak jak powiedział DateTime.UtcNow.

  • Staraj się jak najmniej ufać klientowi, który przekazuje daty na serwer. Na przykład, jeśli potrzebujesz „teraz”, nie twórz daty na kliencie, a następnie przekazuj ją do serwera. Utwórz datę w GET i przekaż ją do ViewModel lub POST do DateTime.UtcNow.

Jak dotąd dość standardowa taryfa, ale tutaj robi się „interesująco”.

  • Jeśli musisz zaakceptować datę od klienta, użyj javascript, aby upewnić się, że dane, które wysyłasz na serwer, są w czasie UTC. Klient wie, w jakiej strefie czasowej się znajduje, więc może z rozsądną dokładnością przeliczyć czasy na UTC.

  • Podczas renderowania widoków używali <time>elementu HTML5 , nigdy nie renderowali dat dat bezpośrednio w ViewModel. Został zaimplementowany jako HtmlHelperrozszerzenie, coś w rodzaju Html.Time(Model.when). To by się renderowało <time datetime='[utctime]' data-date-format='[datetimeformat]'></time>.

    Następnie użyliby javascript do przetłumaczenia czasu UTC na czas lokalny klienta. Skrypt znalazłby wszystkie <time>elementy i użyłby date-formatwłaściwości data do sformatowania daty i zapełnienia zawartości elementu.

W ten sposób nigdy nie musieli śledzić, przechowywać ani zarządzać strefą czasową klientów. Serwer nie przejmował się strefą czasową klienta, ani nie musiał wykonywać żadnych tłumaczeń stref czasowych. Po prostu wypluł UTC i pozwolił klientowi przekształcić go w coś rozsądnego. Co jest łatwe z poziomu przeglądarki, ponieważ wie, w jakiej strefie czasowej się znajduje. Jeśli klient zmieni swoją strefę czasową, aplikacja internetowa zaktualizuje się automatycznie. Jedyne, co zapisali, to łańcuch formatu daty i godziny dla ustawień regionalnych użytkownika.

Nie mówię, że to było najlepsze podejście, ale było to inne, którego wcześniej nie widziałem. Może wyciągniesz z niego kilka interesujących pomysłów.

J. Holmes
źródło
Dzięki za twoją odpowiedź. Czy podczas otrzymywania danych wejściowych od użytkownika (na przykład planowania spotkania) nie zakłada się, że dane wejściowe pochodzą z jego bieżącej strefy czasowej? Co myślisz o ModelBinder wykonującym tę transformację przed wejściem do akcji (zobacz moje komentarze do @casperOne. <time>Pomysł jest całkiem fajny. Jedyną wadą jest to, że muszę odpytać cały DOM pod kątem tych elementów, co sama zmiana dat nie jest przyjemna (a co z wyłączonym JS?).
Jeszcze
Zwykle tak, przy założeniu, że będzie to lokalna strefa czasowa. Postanowili jednak upewnić się, że za każdym razem, gdy zbierają czas od użytkownika, przed wysłaniem go na serwer, jest on przekształcany na UTC za pomocą javascript. Ich rozwiązanie było bardzo ciężkie dla JS i nie uległo degradacji, gdy JS był wyłączony. Nie myślałem o tym wystarczająco dużo, aby zobaczyć, czy jest w tym spryt. Ale tak jak powiedziałem, może da ci to kilka pomysłów.
J. Holmes,
2
Od tamtej pory pracowałem nad innym projektem, który wymagał lokalnych dat i to była metoda, której użyłem. Używam momentu.js do formatowania daty / czasu ... to słodkie. Dzięki!
TheCloudlessSky
Czy nie istnieje prosty sposób, aby zdefiniować w app.config / web.cofig aplikacji strefę czasową zakresu aplikacji i zmienić wszystkie wartości DateTime.Now na DateTime.UtcNow? (Chcę uniknąć sytuacji, w których programiści w firmie nadal używaliby DateTime, teraz przez pomyłkę)
Uri Abramson
3
Wadą tego podejścia, którą widzę, jest to, że poświęcasz czas na inne rzeczy, tworząc pliki PDF, e-maile i tym podobne, nie ma dla nich elementu czasu, więc nadal będziesz musiał je konwertować ręcznie. W przeciwnym razie bardzo zgrabne rozwiązanie
shenku
15

Po kilku informacjach zwrotnych, oto moje ostateczne rozwiązanie, które moim zdaniem jest czyste i proste i obejmuje kwestie związane z czasem letnim.

1 - Konwersję wykonujemy na poziomie modelu. Tak więc w klasie Model piszemy:

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

2 - W globalnym pomocniku tworzymy naszą niestandardową funkcję „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 - Możemy to jeszcze bardziej ulepszyć, zapisując identyfikator strefy czasowej w każdym profilu użytkownika, dzięki czemu możemy pobierać dane z klasy użytkownika zamiast używać stałej wartości „China Standard Time”:

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

4 - Tutaj możemy uzyskać listę stref czasowych, które użytkownik może wybrać z rozwijanego menu:

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

Tak więc teraz o 9:25 w Chinach, strona internetowa hostowana w USA, data zapisana w bazie danych UTC, oto wynik końcowy:

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)

EDYTOWAĆ

Dziękuję Mattowi Johnsonowi za wskazanie słabych części oryginalnego rozwiązania i przepraszam za usunięcie oryginalnego posta, ale mam problemy z uzyskaniem odpowiedniego formatu wyświetlania kodu ... Okazało się, że edytor ma problemy z mieszaniem "punktorów" z "kodem wstępnym", więc ja usunąłem byki i było dobrze.

Nestor
źródło
Wydaje się, że jest to możliwe, jeśli ktoś nie ma architektury wielowarstwowej lub współdzielonych bibliotek internetowych. Jak możemy udostępnić identyfikator strefy czasowej warstwie danych.
Vikash Kumar
9

W sekcji wydarzeń na sf4answers użytkownicy wprowadzają adres wydarzenia, a także datę rozpoczęcia i opcjonalną datę zakończenia. Te czasy są tłumaczone na datetimeoffsetserwer SQL, który uwzględnia przesunięcie względem czasu UTC.

To jest ten sam problem, przed którym stoisz (chociaż podchodzisz do niego inaczej, ponieważ używasz DateTime.UtcNow); masz lokalizację i musisz przetłumaczyć czas z jednej strefy czasowej na drugą.

Zrobiłem dwie główne rzeczy, które mi pomogły. Po pierwsze , zawsze używaj DateTimeOffsetstruktury . Uwzględnia potrącenie z UTC i jeśli możesz uzyskać te informacje od swojego klienta, ułatwi Ci to życie.

Po drugie, wykonując tłumaczenia, zakładając, że znasz lokalizację / strefę czasową, w której znajduje się klient, możesz użyć publicznej bazy danych stref czasowych, aby przetłumaczyć czas z UTC na inną strefę czasową (lub triangulować, jeśli chcesz, między dwiema strefy czasowe). Wspaniałą rzeczą w bazie danych tz (czasami nazywanej bazą danych Olson ) jest to, że uwzględnia ona zmiany stref czasowych w historii; uzyskanie przesunięcia jest funkcją daty, na którą chcesz je uzyskać (wystarczy spojrzeć na ustawę o polityce energetycznej z 2005 r., która zmieniła daty wejścia w życie czasu letniego w USA ).

Mając bazę danych w ręku, możesz użyć interfejsu API ZoneInfo (baza danych tz / baza danych Olson) .NET . Zwróć uwagę, że nie ma dystrybucji binarnej, musisz pobrać najnowszą wersję i samodzielnie ją skompilować.

W chwili pisania tego tekstu analizuje on obecnie wszystkie pliki w najnowszej dystrybucji danych (faktycznie uruchomiłem go z plikiem ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz 25 września, 2011; w marcu 2017 r. Można go było dostać za pośrednictwem https://iana.org/time-zones lub z ftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gz ).

Tak więc w odpowiedziach sf4, po uzyskaniu adresu, jest on geokodowany w kombinacji szerokość / długość geograficzna, a następnie wysyłany do usługi internetowej innej firmy w celu uzyskania strefy czasowej, która odpowiada wpisowi w bazie danych tz. Stamtąd czasy rozpoczęcia i zakończenia są konwertowane na DateTimeOffsetwystąpienia z odpowiednim przesunięciem czasu UTC, a następnie zapisywane w bazie danych.

Jeśli chodzi o radzenie sobie z tym w SO i witrynach internetowych, zależy to od odbiorców i tego, co próbujesz wyświetlić. Jeśli zauważysz, większość serwisów społecznościowych (i SO oraz sekcja wydarzeń w sf4answers) wyświetla zdarzenia w czasie względnym lub, jeśli używana jest wartość bezwzględna, zwykle jest to UTC.

Jeśli jednak Twoi odbiorcy oczekują czasu lokalnego, użycie DateTimeOffsetwraz z metodą rozszerzenia, która pobiera strefę czasową do konwersji, byłoby w porządku; typ danych SQL datetimeoffsetprzetłumaczyłby się na .NET, DateTimeOffsetktóry można następnie uzyskać uniwersalny czas stosowania GetUniversalTimemetody . Stamtąd po prostu używasz metod ZoneInfoklasy do konwersji z UTC na czas lokalny (będziesz musiał trochę popracować, aby wprowadzić go do a DateTimeOffset, ale jest to dość proste).

Gdzie dokonać transformacji? To koszt, który będziesz musiał gdzieś zapłacić , i nie ma „najlepszego” sposobu. Wybrałbym jednak widok, z przesunięciem strefy czasowej jako częścią modelu widoku prezentowanego w widoku. W ten sposób, jeśli zmienią się wymagania dotyczące widoku, nie musisz zmieniać modelu widoku, aby uwzględnić zmianę. Twoja JsonResultpo prostu zawierać model z i offset.IEnumerable<T>

Po stronie wejściowej, używając segregatora modelowego? Powiedziałbym, że nie ma mowy. Nie możesz zagwarantować, że wszystkie daty (teraz lub w przyszłości) będą musiały zostać przekształcone w ten sposób, powinno to być jawną funkcją twojego kontrolera, aby wykonać tę akcję. Ponownie, jeśli wymagania ulegną zmianie, nie musisz dostosowywać jednego lub wielu ModelBinderwystąpień, aby dostosować logikę biznesową; i jest to logika biznesowa, co oznacza, że ​​powinna znajdować się w kontrolerze.

casperOne
źródło
Dziękuję za szczegółową odpowiedź. Mam nadzieję, że nie wydam się taki niegrzeczny, ale to tak naprawdę nie rozwiązało żadnego z moich obaw związanych z ASP.NET MVC: A co z danymi wejściowymi użytkownika (wystarczyłoby użycie niestandardowego segregatora modelu)? A co najważniejsze, co z wyświetlaniem tych dat (w lokalnej strefie czasowej użytkownika)? Czuję, że metoda przedłużenia doda dużo wagi do moich poglądów, co wydaje się niepotrzebne.
TheCloudlessSky
1
@TheCloudlessSky: Zobacz dwa ostatnie akapity mojej zredagowanej odpowiedzi. Osobiście uważam, że szczegóły dotyczące tego, gdzie dokonać transformacji, są niewielkie; głównym problemem jest faktyczna konwersja i przechowywanie danych datetime (przy okazji, nie mogę wystarczająco podkreślić użycia datetimeoffsetw SQL Server i DateTimeOffset.NET tutaj, naprawdę bardzo upraszczają one sprawę), które moim zdaniem .NET nie obsługuje odpowiednio w ogóle z powodów przedstawionych powyżej. Jeśli masz datę w Nowym Jorku, która została wprowadzona w 2003 r., A następnie chcesz ją przetłumaczyć na datę w Los Angeles w 2011 r., W takim przypadku .NET ciężko zawodzi.
casperOne,
Problem z niewykorzystaniem spinacza modelu (lub jakiejkolwiek warstwy przed akcją) polega na tym, że każdy kontroler staje się bardzo trudny do przetestowania podczas pracy z datami (wszystkie zyskałyby zależność od konwersji daty). Umożliwiłoby to zapisywanie dat testów jednostkowych w UTC. Moja aplikacja ma użytkowników, którzy są powiązani z profilem (pomyśl o lekarzach / sekretarkach związanych z ich praktyką). Profil zawiera informacje o strefie czasowej. Dlatego dość łatwo jest uzyskać strefę czasową profilu bieżącego użytkownika i przekształcić ją podczas wiązania. Czy masz inne argumenty przeciwko temu? Dzięki za wkład!
TheCloudlessSky
Argumentem przeciwko temu jest to, że testy nie odzwierciedlałyby dokładnie przypadków testowych. Twoje dane wejściowe nie będą w UTC, więc twoje przypadki testowe nie powinny być spreparowane, aby ich używać. Powinien używać rzeczywistych dat z lokalizacjami i wszystkim (chociaż jeśli użyjesz tego DateTimeOffset, bardzo to złagodzisz, IMO).
casperOne
Tak - ale to oznacza, że każdy test, który dotyczy dat, musi to uwzględniać. Każde działanie, które dotyczy dat , zawsze będzie konwertowane na UTC. Dla mnie jest to główny kandydat na segregator modelowy, a następnie przetestuj segregator modelowy .
TheCloudlessSky
5

To tylko moja opinia, uważam, że aplikacja MVC powinna dobrze oddzielać problem prezentacji danych od zarządzania modelem danych. Baza danych może przechowywać dane w czasie lokalnego serwera, ale obowiązkiem warstwy prezentacji jest renderowanie daty i godziny przy użyciu lokalnej strefy czasowej użytkownika. Wydaje mi się, że to ten sam problem, co I18N i format liczb dla różnych krajów. W Twoim przypadku aplikacja powinna wykryć Culturestrefę czasową użytkownika i zmienić Widok, pokazując inny tekst, liczbę i datę, ale przechowywane dane mogą mieć ten sam format.

Massimo Zerbini
źródło
Dzięki za twoją odpowiedź. Tak, to jest to, co w zasadzie opisałem, po prostu zastanawiam się, jak można to elegancko osiągnąć za pomocą MVC. Aplikacja przechowuje strefę czasową użytkownika w ramach tworzenia konta (wybiera on strefę czasową). Ponownie pojawia się problem związany z renderowaniem dat w każdym widoku bez (miejmy nadzieję) konieczności zaśmiecania ich metodami, które stworzyłem DateTimeExtensions.
TheCloudlessSky
Jest na to wiele sposobów, jednym jest użycie metod pomocniczych, jak zasugerowałeś, innym, być może bardziej złożonym, ale eleganckim, jest użycie filtru, który przetwarza żądania i odpowiedzi oraz konwertuje datę i godzinę. Innym sposobem jest opracowanie niestandardowego atrybutu Display w celu dodania adnotacji do pól typu DateTime w modelu widoku.
Massimo Zerbini
To są rzeczy, których szukałem. Jeśli spojrzysz na mój OP, wspomniałem również o używaniu AutoMappera do przetwarzania przed przejściem do wyniku view / json, ale po wykonaniu akcji.
TheCloudlessSky
1

W przypadku danych wyjściowych utwórz szablon wyświetlania / edytora, taki jak ten

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

Możesz powiązać je na podstawie atrybutów w modelu, jeśli chcesz, aby tylko niektóre modele korzystały z tych szablonów.

Zobacz tutaj i tutaj, aby uzyskać więcej informacji na temat tworzenia niestandardowych szablonów edytorów.

Alternatywnie, ponieważ chcesz, aby działał zarówno dla wejścia, jak i wyjścia, sugerowałbym rozszerzenie kontroli lub nawet utworzenie własnej. W ten sposób możesz przechwycić zarówno dane wejściowe, jak i wyjściowe i przekonwertować tekst / wartość w razie potrzeby.

Mam nadzieję, że ten link popchnie cię we właściwym kierunku, jeśli chcesz iść tą ścieżką.

Tak czy inaczej, jeśli chcesz eleganckiego rozwiązania, będzie to trochę pracy. Z drugiej strony, kiedy już to zrobisz, możesz zachować to w swojej bibliotece kodu do wykorzystania w przyszłości!

dnatoli
źródło
Myślę, że niestandardowe szablony wyświetlania / edytora i segregatora to najbardziej eleganckie rozwiązanie, ponieważ po wdrożeniu będzie „po prostu działać”. Żaden programista nie będzie musiał wiedzieć nic specjalnego, aby daty działały prawidłowo, po prostu użyj DisplayFori EditorForbędzie działać za każdym razem. +1
Kevin Stricker
17
czy nie spowodowałoby to renderowania czasu serwera aplikacji, a nie przeglądarki?
Alex
0

Jest to prawdopodobnie młot kowalski do złamania orzecha, ale można wprowadzić warstwę między warstwą interfejsu użytkownika a warstwą biznesową, która w sposób przejrzysty konwertuje czasy danych na czas lokalny na zwracanych grafach obiektów oraz na czas UTC na wejściowych parametrach daty i godziny.

Wyobrażam sobie, że można to osiągnąć za pomocą PostSharp lub jakiejś inwersji kontenera sterującego.

Osobiście wybrałbym po prostu jawną konwersję dat w interfejsie użytkownika ...

geoffreys
źródło