У меня есть веб-сайт, который размещен в другом часовом поясе, чем пользователи, использующие приложение. В дополнение к этому у пользователей может быть определенный часовой пояс. Мне было интересно, как к этому подходят другие пользователи и приложения SO? Наиболее очевидная часть заключается в том, что внутри БД дата и время хранятся в формате UTC. На сервере все дата / время должны обрабатываться в формате UTC. Однако я вижу три проблемы, которые пытаюсь преодолеть:
Получение текущего времени в формате UTC (легко решается с помощью
DateTime.UtcNow
).Получение даты / времени из базы данных и отображение их пользователю. Существует потенциально много вызовов для печати дат в разных представлениях. Я думал о каком-то промежуточном уровне между представлением и контроллерами, который мог бы решить эту проблему. Или иметь собственный метод расширения
DateTime
(см. Ниже). Основным недостатком является то, что в каждом месте использования даты и времени в представлении необходимо вызывать метод расширения!Это также усложнило бы использование чего-то вроде
JsonResult
. Вы уже не могли бы легко позвонитьJson(myEnumerable)
, это должно было бытьJson(myEnumerable.Select(transformAllDates))
. Может быть, AutoMapper поможет в этой ситуации?Получение ввода от пользователя (локально по UTC). Например, для POST-формы формы с датой потребуется преобразовать дату в формат UTC раньше. Первое, что приходит в голову, - это создание кастома
ModelBinder
.
Вот расширения, которые я думал использовать в представлениях:
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);
}
}
Я думаю, что работа с часовыми поясами будет такой распространенной вещью, учитывая, что многие приложения теперь работают в облаке, где местное время сервера может сильно отличаться от ожидаемого часового пояса.
Было ли это элегантно решено раньше? Что мне не хватает? Идеи и мысли очень ценятся.
РЕДАКТИРОВАТЬ: Чтобы устранить некоторую путаницу, я подумал добавить еще несколько деталей. Проблема прямо сейчас не в том, как сохранить время UTC в базе данных, а в процессе перехода от UTC-> Local и Local-> UTC. Как указывает @Max Zerbini, очевидно, разумно поместить код UTC-> Local в представление, но DateTimeExtensions
действительно ли используется ответ? При получении ввода от пользователя имеет ли смысл принимать даты как местное время пользователя (поскольку это то, что будет использовать JS), а затем использовать a ModelBinder
для преобразования в UTC? Часовой пояс пользователя хранится в БД и его легко получить.
источник
ModelBinder
.Ответы:
Не то, чтобы это рекомендация, это больше разделение парадигмы, но наиболее агрессивный способ обработки информации о часовом поясе в веб-приложении (который не является эксклюзивным для ASP.NET MVC), который я видел, был следующим:
Все даты на сервере указаны в формате UTC. Это означает , используя, как вы сказали,
DateTime.UtcNow
.Старайтесь как можно меньше доверять клиенту, который передает даты на сервер. Например, если вам нужно «сейчас», не создавайте дату на клиенте, а затем передавайте ее на сервер. Либо создайте дату в своем GET и передайте ее в ViewModel, либо в POST
DateTime.UtcNow
.Пока что довольно стандартная плата за проезд, но здесь все становится «интереснее».
Если вам нужно принять дату от клиента, используйте javascript, чтобы убедиться, что данные, которые вы отправляете на сервер, находятся в UTC. Клиент знает, в каком часовом поясе находится, поэтому может с разумной точностью преобразовать время в UTC.
При рендеринге представлений они использовали
<time>
элемент HTML5 , они никогда не отображали дату и время непосредственно в ViewModel. Он был реализован какHtmlHelper
расширение, что-то вродеHtml.Time(Model.when)
. Это будет рендерить<time datetime='[utctime]' data-date-format='[datetimeformat]'></time>
.Затем они использовали бы javascript для перевода времени UTC в местное время клиента. Сценарий найдет все
<time>
элементы и используетdate-format
свойство data для форматирования даты и заполнения содержимого элемента.Таким образом, им никогда не приходилось отслеживать, хранить или управлять часовым поясом клиентов. Сервер не заботился о том, в каком часовом поясе находится клиент, и ему не нужно было делать какие-либо переводы часовых поясов. Он просто выплевывает UTC и позволяет клиенту преобразовать это во что-то разумное. Это легко сделать из браузера, потому что он знает, в каком часовом поясе он находится. Если клиент изменил свой часовой пояс, веб-приложение автоматически обновится. Единственное, что они сохранили, - это строка формата datetime для локали пользователя.
Я не говорю, что это был лучший подход, но это был другой подход, которого я раньше не видел. Может быть, вы почерпнете из него интересные идеи.
источник
<time>
идея довольно крутая. Единственным недостатком является то, что это означает, что мне нужно запросить всю DOM для этих элементов, что не совсем хорошо просто преобразовывать даты (как насчет отключенного JS?).После нескольких отзывов, вот мое окончательное решение, которое, на мой взгляд, чистое и простое и охватывает проблемы перехода на летнее время.
1 - Мы занимаемся преобразованием на уровне модели. Итак, в классе Model мы пишем:
public class Quote { ... public DateTime DateCreated { get { return CRM.Global.ToLocalTime(_DateCreated); } set { _DateCreated = value.ToUniversalTime(); } } private DateTime _DateCreated { get; set; } ... }
2 - В глобальном помощнике мы делаем нашу пользовательскую функцию «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 - Мы можем улучшить это дальше, сохранив идентификатор часового пояса в каждом профиле пользователя, чтобы мы могли извлекать из класса пользователя вместо использования константы «Китайское стандартное время»:
public class Contact { ... public string TimeZone { get; set; } ... }
4 - Здесь мы можем получить список часовых поясов для отображения пользователю для выбора из раскрывающегося списка:
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; } }
Итак, сейчас в 9:25 утра в Китае, веб-сайт размещен в США, дата сохраняется в базе данных в формате UTC, вот окончательный результат:
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)
РЕДАКТИРОВАТЬ
Спасибо Мэтту Джонсону за указание на слабые стороны исходного решения и извините за удаление исходного сообщения, но возникли проблемы с получением правильного формата отображения кода ... оказалось, что у редактора проблемы со смешиванием «маркеров» с «предварительным кодом», поэтому я удалил пули, и все было в порядке.
источник
В разделе событий на sf4answers пользователи вводят адрес события, а также дату начала и необязательную дату окончания. Это время переводится
datetimeoffset
в сервер SQL, который учитывает смещение от UTC.Это та же проблема, с которой вы сталкиваетесь (хотя вы используете другой подход к ней, который вы используете
DateTime.UtcNow
); у вас есть местоположение, и вам нужно перевести время из одного часового пояса в другой.Я сделал две вещи, которые помогли мне. Во-первых , всегда используйте
DateTimeOffset
структуру . Он учитывает смещение от UTC, и если вы можете получить эту информацию от своего клиента, это сделает вашу жизнь немного проще.Во-вторых, при выполнении переводов, предполагая, что вы знаете местоположение / часовой пояс, в котором находится клиент, вы можете использовать базу данных часовых поясов общедоступной информации для перевода времени из UTC в другой часовой пояс (или триангулировать, если хотите, между двумя часовые пояса). В базе данных tz (иногда называемой базой данных Олсона ) замечательно то, что она учитывает изменения часовых поясов на протяжении всей истории; получение смещения является функцией даты, на которую вы хотите получить смещение (просто посмотрите на Закон об энергетической политике 2005 года, который изменил даты, когда в США вступает в силу летнее время ).
Имея под рукой базу данных, вы можете использовать .NET API ZoneInfo (tz database / Olson database) . Обратите внимание, что двоичного дистрибутива нет, вам придется скачать последнюю версию и скомпилировать ее самостоятельно.
На момент написания этой статьи он в настоящее время анализирует все файлы в последнем распределении данных (я фактически запускал его с файлом ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz 25 сентября, 2011 г .; в марте 2017 г. его можно было получить через https://iana.org/time-zones или на ftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gz ).
Итак, в sf4answers после получения адреса он геокодируется в комбинацию широты и долготы, а затем отправляется сторонней веб-службе для получения часового пояса, соответствующего записи в базе данных tz. Отсюда время начала и окончания преобразуется в
DateTimeOffset
экземпляры с правильным смещением UTC, а затем сохраняется в базе данных.Что касается работы с ним на SO и веб-сайтах, это зависит от аудитории и того, что вы пытаетесь отобразить. Если вы заметили, большинство социальных сайтов (и SO, и раздел событий на sf4answers) отображают события в относительном времени или, если используется абсолютное значение, обычно это UTC.
Однако, если ваша аудитория ожидает местного времени, то использование
DateTimeOffset
метода расширения, который использует часовой пояс для преобразования, будет вполне приемлемым; тип данных SQLdatetimeoffset
будет преобразован в .NET,DateTimeOffset
который затем вы можете получить всемирное время для использования этогоGetUniversalTime
метода . Оттуда вы просто используете методыZoneInfo
класса для преобразования из UTC в местное время (вам придется немного поработать, чтобы преобразовать его в aDateTimeOffset
, но это достаточно просто сделать).Где сделать трансформацию? Это стоимость, которую вам придется где-то заплатить , и нет "лучшего" способа. Однако я бы выбрал представление со смещением часового пояса как частью модели представления, представленной в представлении. Таким образом, если требования к представлению изменятся, вам не нужно будет изменять модель представления, чтобы учесть это изменение. Вы
JsonResult
бы просто содержать модель с и смещением.IEnumerable<T>
На стороне ввода, используя подшивку модели? Я бы сказал абсолютно никак. Вы не можете гарантировать, что все даты (сейчас или в будущем) должны быть преобразованы таким образом, это должно быть явной функцией вашего контроллера для выполнения этого действия. Опять же, если требования меняются, вам не нужно настраивать один или несколько
ModelBinder
экземпляров, чтобы настроить бизнес-логику; и это является бизнес - логика, которая означает , что она должна быть в контроллере.источник
datetimeoffset
в SQL Server иDateTimeOffset
в .NET, они действительно значительно упрощают вещи), которые, как я считаю, .NET не обрабатывает должным образом вообще по причинам, изложенным выше. Если у вас есть дата в Нью-Йорке, которая была введена в 2003 году, а затем вы хотите, чтобы она была переведена на дату в Лос-Анджелесе в 2011 году, .NET в этом случае не справится.DateTimeOffset
вы значительно смягчите это, ИМО).Это всего лишь мое мнение, я считаю, что приложение MVC должно отделять проблему представления данных от управления моделью данных. База данных может хранить данные в локальном времени сервера, но уровень представления обязан отображать datetime с использованием часового пояса локального пользователя. Мне кажется, что это та же проблема, что и I18N и формат чисел для разных стран. В вашем случае ваше приложение должно определять
Culture
часовой пояс пользователя и изменять представление, отображающее различный текст, числа и представление даты, но сохраненные данные могут иметь тот же формат.источник
DateTimeExtensions
.Для вывода создайте шаблон отображения / редактора, подобный этому
Вы можете связать их на основе атрибутов вашей модели, если хотите, чтобы только определенные модели использовали эти шаблоны.
См. Здесь и здесь для получения дополнительных сведений о создании пользовательских шаблонов редактора.
В качестве альтернативы, поскольку вы хотите, чтобы он работал как для ввода, так и для вывода, я бы предложил расширить элемент управления или даже создать свой собственный. Таким образом, вы можете перехватить как ввод, так и вывод и преобразовать текст / значение по мере необходимости.
Надеюсь, эта ссылка подтолкнет вас в правильном направлении, если вы хотите пойти по этому пути.
В любом случае, если вам нужно элегантное решение, придется немного поработать. С другой стороны, как только вы это сделаете, вы сможете сохранить его в своей библиотеке кода для будущего использования!
источник
DisplayFor
и ,EditorFor
и он будет работать каждый раз. +1Это, вероятно, кувалда, чтобы расколоть орешек, но вы можете вставить слой между пользовательским интерфейсом и бизнес-уровнями, который прозрачно преобразует дату и время в местное время на возвращаемых графах объектов и в UTC для входных параметров datetime.
Я предполагаю, что этого можно добиться с помощью PostSharp или некоторой инверсии контейнера управления.
Лично я бы просто явно преобразовал ваши даты в пользовательский интерфейс ...
источник
Я хотел хранить даты как DateTimeOffset, чтобы можно было поддерживать смещение часового пояса пользователя, который выполняет запись в базу данных. Однако я хотел использовать DateTime только внутри самого приложения.
Итак, местный часовой пояс внутри, местный часовой пояс вне. Независимо от того, кто / где / когда пользователь просматривает данные, для наблюдателя это будет местное время, а изменения сохраняются как UTC + локальное смещение.
Вот как я этого добился.
1. Во-первых, мне нужно было получить смещение местного часового пояса веб-клиента и сохранить это значение на веб-сервере:
// Sets a session variable for local time offset from UTC function SetTimeZone() { var now = new Date(); var offset = now.getTimezoneOffset() / 60; var sign = offset > 0 ? "-" : "+"; var offset = "0" + offset; offset = sign + offset + ":00"; $.ajax({ type: "post", url: prefixWithSitePathRoot("/Home/SetTimeZone"), data: { OffSet: offset }, datatype: "json", traditional: true, success: function (data) { var data = data; }, error: function (XMLHttpRequest, textStatus, errorThrown) { alert("SetTimeZone failed"); } }); }
Формат предназначен для соответствия формату типа DateTimeOffset SQL Server.
SetTimeZone - просто устанавливает значение переменной Session. Когда пользователь входит в систему, я включаю это значение в кэш профиля пользователя.
2. Когда пользователь отправляет изменение в базу данных, я фильтрую значение DateTime с помощью служебного класса:
cmdADO.Parameters.AddWithValue("@AwardDate", (object)Utility.ConvertLocal2UTC(theContract.AwardDate, theContract.TimeOffset) ?? DBNull.Value);
Метод:
public static DateTimeOffset? ConvertLocal2UTC(DateTime? theDateTime, string TimeZoneOffset) { DateTimeOffset? DtOffset = null; if (null != theDateTime) { TimeSpan AmountOfTime; TimeSpan.TryParse(TimeZoneOffset, out AmountOfTime); DateTime datetime = Convert.ToDateTime(theDateTime); DateTime datetimeUTC = datetime.ToUniversalTime(); DtOffset = new DateTimeOffset(datetimeUTC.Ticks, AmountOfTime); } return DtOffset; }
3. Когда я читаю дату с SQL Server, я делаю следующее:
theContract.AwardDate = theRow.IsNull("AwardDate") ? new Nullable<DateTime>() : DateTimeOffset.Parse(Convert.ToString(theRow["AwardDate"])).DateTime;
В контроллере я изменяю datetime, чтобы оно соответствовало местному времени наблюдателя. (Я уверен, что кто-то может сделать лучше с расширением или чем-то еще):
Метод:
public static DateTime? ConvertUTC2Local(DateTime? theDateTime, string TimeZoneOffset) { if (null != theDateTime) { TimeSpan AmountOfTime; TimeSpan.TryParse(TimeZoneOffset, out AmountOfTime); DateTime datetime = Convert.ToDateTime(theDateTime); datetime = datetime.Add(AmountOfTime); theDateTime = new DateTime(datetime.Ticks, DateTimeKind.Utc); } return theDateTime; }
В представлении я просто отображаю / редактирую / проверяю DateTime.
Надеюсь, это поможет кому-то, у кого есть подобная потребность.
источник