Наше приложение представляет собой большое n-уровневое приложение ASP.NET MVC, которое сильно зависит от дат и (местного) времени. До сих пор мы использовали DateTime
для всех наших моделей, которые работали нормально, потому что в течение многих лет мы были строго национальным веб-сайтом, работающим с одним часовым поясом.
Теперь все изменилось, и мы открываем двери для международной аудитории. Первой мыслью было «Вот дерьмо. Нам нужно провести рефакторинг всего нашего решения!»
TimeZoneInfo
Мы открыли LinQPad и начали набрасывать различные преобразователи для преобразования обычных DateTime
объектов в DateTimeOffset
объекты на основе объекта TimeZoneInfo
, который был создан на основе значения идентификатора часового пояса пользователя из указанного профиля пользователя.
Мы решили, что изменим все DateTime
свойства в моделях на DateTimeOffset
и покончим с этим. В конце концов, теперь у нас была вся информация, необходимая для хранения и отображения местной даты и времени пользователя.
Большая часть фрагментов кода была вдохновлена сообщение в блоге Рика Штрала по этой теме.
NodaTime и DateTimeOffset
Но потом я прочитал Превосходный комментарий Мэтта Джонсона. Он подтвердил мое намерение перейти на DateTimeOffset
, заявив: «DateTimeOffset необходим в веб-приложении».
Что касается Noda Time, Мэтт говорит:
Говоря о Noda Time, я не соглашусь с вами, что вам нужно заменить все в вашей системе. Конечно, если вы это сделаете, у вас будет намного меньше возможностей ошибиться, но вы, безусловно, можете просто использовать Noda Time там, где это имеет смысл. Я лично работал над системами, которые должны были преобразовывать часовые пояса с использованием часовых поясов IANA (например, «America / Los_Angeles»), но отслеживал все остальное в типах DateTime и DateTimeOffset. На самом деле довольно часто можно увидеть, что Noda Time широко используется в логике приложения, но полностью исключен из DTO и уровней сохраняемости. В некоторых технологиях, таких как Entity Framework, вы не могли использовать Noda Time напрямую, если бы захотели - потому что его некуда подключить.
Это могло быть направлено непосредственно на нас, поскольку прямо сейчас мы находимся в том же сценарии, включая наш выбор использовать часовые пояса IANA.
Наш план, хороший или плохой?
Наша основная цель - создать наименее сложный рабочий процесс для работы с датами и временем в различных часовых поясах. По возможности избегайте расчетов часовых поясов в наших Сервисах, репозиториях и контроллерах.
Вкратце, план состоит в том, чтобы принимать местные даты и время из нашего внешнего интерфейса, преобразовывать их как можно скорее в ZonedDateTime
и преобразовывать их в DateTimeOffset
как можно позже, непосредственно перед сохранением информации в базе данных.
Ключевым фактором в определении правильного ZonedDateTime
является свойство TimeZoneId
в модели пользователя.
public class ApplicationUser : IdentityUser
{
[Required]
public string TimezoneId { get; set; }
}
Местное DateTime в NodaTime
Чтобы избежать большого количества повторяющегося кода, мы планируем создать пользовательские привязки моделей, которые преобразуют локальные DateTime
в ZonedDateTime
.
public class LocalDateTimeModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
// Get the posted local datetime
string dt = request.Form.Get("DateTime");
DateTime dateTime = DateTime.Parse(dt);
// Get the logged in User
IPrincipal p = controllerContext.HttpContext.User;
var user = p.ApplicationUser();
// Convert to ZonedDateTime
LocalDateTime localDateTime = LocalDateTime.FromDateTime(dateTime);
IDateTimeZoneProvider timeZoneProvider = DateTimeZoneProviders.Tzdb;
var usersTimezone = timeZoneProvider[user.TimezoneId];
var zonedDbDateTime = usersTimezone.AtLeniently(localDateTime);
return zonedDbDateTime;
}
}
Мы можем засорять наши контроллеры этими переплетами моделей.
[HttpPost]
[Authorize]
public ActionResult SimpleDateTime([ModelBinder(typeof (LocalDateTimeModelBinder))] ZonedDateTime dateTime)
{
// Do stuff with the ZonedDateTime object
}
Мы слишком много думаем об этом?
Хранение DateTimeOffset в БД
Мы будем использовать концепцию свойств Buddy. Честно говоря, я не большой поклонник этого из-за путаницы, которую это создает. Новые разработчики, вероятно, будут бороться с тем фактом, что у нас есть более одного способа сохранить дату создания.
Мы очень приветствуем предложения о том, как это улучшить. Я читал комментарии о сокрытии свойств из IntelliSense для установки реальных свойств на private
.
public class Item
{
public int Id { get; set; }
public string Title { get; set; }
// The "real" property
public DateTimeOffset DateCreated { get; private set; }
// Buddy property
[NotMapped]
public ZonedDateTime CreatedAt
{
get
{
// DateTimeOffset to NodaTime, based on User's TZ
return ToZonedDateTime(DateCreated);
}
// NodaTime to DateTimeOffset
set { DateCreated = value.ToDateTimeOffset(); }
}
public string OwnerId { get; set; }
[ForeignKey("OwnerId")]
public virtual ApplicationUser Owner { get; set; }
// Helper method
public ZonedDateTime ToZonedDateTime(DateTimeOffset dateTime, string tz = null)
{
if (string.IsNullOrEmpty(tz))
{
tz = Owner.TimezoneId;
}
IDateTimeZoneProvider timeZoneProvider = DateTimeZoneProviders.Tzdb;
var usersTimezoneId = tz;
var usersTimezone = timeZoneProvider[usersTimezoneId];
var zonedDate = ZonedDateTime.FromDateTimeOffset(dateTime);
return zonedDate.ToInstant().InZone(usersTimezone);
}
}
Все между
Теперь у нас есть приложение на основе Noda Time. Объект ZonedDateTime упрощает выполнение специальных вычислений и запросов на основе часовых поясов.
Это правильное предположение?
ApplicationUser.TimezoneId
, верно? Или могут быть случаи, когда один пользователь просматривает данные другого пользователя? - person Matt Johnson-Pint   schedule 16.10.2015ZonedDateTime
- person Fred Fickleberry III   schedule 16.10.2015