Jak automatycznie zmieniać czas lokalny na UTC w ASP.NET MVC?

Wprowadzenie

W bardzo wielu aplikacjach w bazie danych zapisujemy ten sam czas jaki przyszedł do nas od użytkownika w formularzu. W przypadku, gdy tworzysz aplikację na rynek polski, takie podejście może okazać się bezproblemowe, bo wszyscy jesteśmy w jednej strefie czasowej i mamy ten sam czas. Problemy zaczynają się, gdy tworzymy aplikację, w której użytkownicy są w różnych strefach . Wtedy takie podejście może się okazać bardzo problematyczne.

Dlatego zaleca się, aby podczas zapisu daty do bazy danych zmienić jest strefę czasową z lokalnej na UTC. Następie podczas odczytu zrobić odwrotną rzeczy, czy zamienić czas w UTC na lokalny dla danego użytkownika. Dzięki takiemu podejściu dwóch użytkowników w różnych strefach czasowych zobaczy różną datę, ale ten sam moment w czasie.

Najlepiej taką zamianę zautomatyzować w aplikacji, abyśmy mieli pewność, że wszystkie daty w bazie danych są w UTC. Myślę, że od braku zamiany daty na UTC gorszym jest tylko jej częściowa zamiana (cześć dat zamieniamy na UTC, a część nie). Poniżej pokaże jak zrobić automatyczną zamianę daty w aplikacji ASP.NET MVC. Podejście można również przenieść na inne typu aplikacji.

Gdzie jaki czas?

W pierwszej kolejności zaczniemy od określenia jaki czas znajduje się, w której części aplikacji. Jak już wiele razy wspominałem w swoich aplikacjach wykorzystuje ideę viewmodeli. Służą one do przekazywania danych do widoków oraz z drugiej strony dane od użytkownika są do nich bindowane. Dlatego w viewmodelach będzie czas lokalny.

W pozostałych częściach aplikacji używam klas modeli (na podstawie których między innymi jest generowana baza danych przez Entity Framework). W modelach będzie czas w UTC. Sama zamiana czasu lokalnego na UTC oraz w drugą będzie odbywała się podczas zmiany modelu na viewmodel i na odwrót. Wykorzystamy do tego automappera i konwertera typów.

DateTime.Kind

Type DateTime posiada właściwość Kind, którą wykorzystamy podczas konwersji czasu. Właściwość może przyjąć jedną z wartości:

  • Local – informacja, że czas jest lokalny
  • Utc – informacja, że czas jest w UTC
  • Unspecified – informacja, że nie można określić w jakim formacie jest czas

Dla dwóch pierwszych przypadków logika zamiany czasu jest prosta. Problem pojawia się dla ostatniej opcji. Dodatkowo jak sprawdzimy, to okaże się, że domyślny model binder w ASP.NET MVC podczas bindowania danych również ustawia właściwość Kind na Unspecified. Podobnie jest w przypadku Entity Framework. Również tam DateTime ma ustawioną właściwość Kind na Unspecified.

Dlatego potrzebujemy jakiegoś założenia, które ułatwi nam pracę. Jak wspomniałem wyżej tylko w viewmodelach chce mieć czas lokalny, więc możemy trochę zmodyfikować domyślny model binder i w nim ustawiać właściwość Kind na Local. Natomiast w każdym innym miejscu aplikacji będziemy traktować Unspecified jako Utc.

Model Binder dla DateTime

Zmiana zachowania domyślnego model bindera nie jest trudna. Wystarczy utworzyć nową klasę, która dziedziczy po DefaultModelBinder i w niej nadpisać metodę BindModel:

W pierwszej kolejności wywołujemy bazową metodę BindModel, aby DateTime został poprawnie zbindowany. Następnie sprawdzamy, czy bindowany typ jest DateTime (lub nullowalną wersją) i odpowiednia za pomocą metody SpecifyKind ustawiamy informację, że czas jest lokalny.

Na końcu trzeba jeszcze dodać tak utworzony model binder do ASP.NET MVC:

W dedykowanej klasie dla konfiguracji model binderów dodajemy DateTimeBinder dla DateTime oraz DateTime?. Następnie wywołujemy metodę Configure z ModelBindersConfig w klasie MvcApplication z pliku Global.asax.cs.

W ten sposób, czas w viewmodel będzie miał informacje, że jest czasem lokalnym.

Jaka strefa czasowa użytkownika?

Najtrudniejszym elementem całej układanki jest określenie w jakiej strefie czasowej znajduje się użytkownik. Niestety w żądaniu HTTP nie otrzymuje automatycznie takiej informacji. Musimy sami ją ustalić.

Rozwiązuje się to na różne sposoby. W niektórych aplikacjach jest to po prostu ustawienie, które użytkownik może zmienić i dostosować do swoich potrzeba. Czasami strefę czasową odgaduje się na podstawie adresu IP użytkownika. Jeszcze innym rozwiązaniem jest bazowanie na strefie czasowej wykrytej w javascript i przesłanie jej do serwera. W przykładzie wykorzystam ten ostatni sposób.

Przykładowa aplikacja została utworzona z domyślnego szablonu, w którym jest dodane jquery (dodałem jeszcze plugin do jquery do obsługi plików cookie), dlatego je wykorzystam. Cała logika wykrywania strefy czasowej znajduje się w pliku core.js, który jest ładowany w pliku layout, przez co będzie wykonywał się na każdej stronie aplikacji:

Po załadowaniu strony, wykona się metoda setTimezone, która wykrywa z przeglądarki strefę czasową i zapisuje ją w pliku cookies, który później będzie odczytany w aplikacji ASP.NET MVC podczas konwersji czasu. Te rozwiązanie ma jeden minus. W pierwszy żądaniu nie mamy informacji o strefie czasowej i zostanie użyta domyślna strefa. Z drugiej strony na ogół to nie jest duży problem, ponieważ w większości aplikacji użytkownik musi się pierw zalogować, więc w widoku logowania już tą informację uzyskamy. Możemy również w przypadku, gdy nie mamy informacji o strefie czasowej z javascript posłużyć się informacją o strefie na podstawie adresu IP użytkownika.

Mając już informacje o strefie użytkownika możemy przejść do właściwej konwersji dat.

TypeConverter dla DateTime

Do automatycznej konwersji wykorzystamy TypeConvertery z automappera. W tym przypadku konwerter będzie trochę specyficzny, bo mapowanie będzie z DateTime na DateTime:

W pierwszej kolejności sprawdzamy, czy Kind nie jest czasem ustawiona na Unspecified. Gdy tak jest to ustawiamy ją na UTC (według wcześniejszego założenia). Później w zależności od tego, czy czas jest lokalny, czy UTC wywołujemy odpowiednią metodę, która dokona konwersji czasu. Samą konwersję wykona specjalna usługa do operacji na datach (kod niżej).

Na koniec pozostaje jeszcze dodanie konwertera do automappera:

Cała logika konwersji jest zamknięta DateService. Kod usługi wyglada następująco:

Prywatna metoda GetTimeZone służy do pobrania informacji o strefie czasowej użytkownika. Tutaj właśnie odczytujemy informację z pliku cookies i na wartości z niego wyszukujemy odpowiedniej strefy czasowej. Po drodze musimy jeszcze rozwiązać problem z formatem nazwy stref czasowych. Wartość z javascript ma inny format niż oczekuje klasa TimeZoneInfo. Do konwersji wykorzystałem bibliotekę NodeTime (https://nodatime.org/). Jest to bardzo fajna biblioteka do operacji na data w .NET. W przyszłości planuje parę wpisów na jej temat.

Sama konwersja jest wykonywana przez klasę TimeZoneInfo, gdzie przekazujemy informacje o strefie czasowej.

Sam DateService może służyć również do innych celów. Kiedyś wcześniej opisywałem jak użyć DateService do pobierania aktualnego czasu oraz podróży w czasie.

Events – przykład użycia

W testowej aplikacji na github (https://github.com/danielplawgo/WorkWithDataInAspNetMvc) przygotowałem prosty przykład użycia powyższego mechanizmu.

Stworzyłem bardzo prosty mechanizm dodawania wydarzeń, na których przetestujemy automatyczną konwersję czasu dla początku oraz końca wydarzenia. Kod wygląda tak:

Przykład jest dość prosty. W kontrolerze są trzy akcje. Index, która wyświetla dane zapisane w bazie (tutaj już nie rozbudowywałem przykładu i w kontrolerze bezpośrednio korzystam z data contextu, co na dłuższą metę nie jest dobrym rozwiązaniem). Dwie kolejne to obsługa dodania nowego wydarzenia.

W widoku Create wybieramy początek i koniec wydarzenia. Czas jest w formacie lokalnym:

datetime converter create view

Po zapisaniu formularza w bazie czas już jest w UTC (widać to po przesunięciu czasu o dwie godziny, które mamy w Polsce latem w stosunku do UTC):

datetime converter database

Po wyświetleniu widoku Index z wszystkimi wydarzeniami w bazie znowu widzimy czas lokalny:

datetime converter index view

Przykład

Na github znajduje się przykład (https://github.com/danielplawgo/WorkWithDataInAspNetMvc). Aby go uruchomić należy w web.config ustawić poprawny connection string do bazy. Pierwsze uruchomienie spowoduje wygenerowanie struktury bazy oraz dodanie testowych wydarzeń.

WebApi?

W tym wpisie opisuje sytuacje, gdy w aplikacji po stronie serwera generujemy widoku. Zupełnie inna sytuacja jest, gdy na przykład aplikacja jest podzielona na dwie części: backend w web api, natomiast frontend w np. angularze (lub jakaś inna podobna sytuacja). Wtedy jest dużo prościej po stronie backend. Tak naprawdę zawsze pracujemy na czasie UTC. Sama konwersja czasu powinna być wykonana po stronie frontendu. Ale to już temat na zupełnie nowy wpis.

Podsumowanie

Warto być świadomy problemu różnych stref czasowych, którego możemy doświadczyć podczas rozwoju aplikacji. Dlatego warto już wcześniej przygotować jakie rozwiązanie, które najlepiej w sposób, automatyczny będzie rozwiązywało ten problem. Jak widzisz takie rozwiązanie w ASP.NET MVC nie musi być czymś skomplikowanym. Kolejny raz z pomocą przychodzi automapper i konwertery typów (podobnie jak w przypadku lokalizacji enumów). Zachęcam do pobrania przykładu z githuba i przetestowaniu rozwiązania.

A jak Ty rozwiązujesz ten problem z datami? Też masz jakieś automatyczne rozwiązanie?

3 thoughts on “Jak automatycznie zmieniać czas lokalny na UTC w ASP.NET MVC?

  • Pingback: dotnetomaniak.pl
    • Hej Łukaszu!

      Dzięki za komentarz. Masz oczywiście rację, dużo lepszym rozwiązaniem jest skorzystanie w aplikacji z DateTimeOffset (planuje do tego wrócić w przyszłości jako alternatywny sposób rozwiązania z artykułu).

      Z drugiej strony liczba aplikacji, która używa DateTime jako główny typ daty jest tak duża, że zdecydowałem się napisać ten artykuł. Myślę, że czasami łatwiej jest dodać mechanizm z artykułu niż wszystko od razu zmieniać.

      Szkoda, że praktycznie we wszystkich materiałach edukacyjnych pokazywany jest DateTime jako główny typu daty i przez to prawie wszyscy go używają.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *