Наше приложение представляет собой большое приложение ASP.NET MVC n-уровня, которое сильно зависит от дат и (локальных) времен. До сих пор мы использовали DateTime
для всех наших моделей, которые отлично работали, потому что годами мы были строго национальным сайтом, работающим с одним часовым поясом.
Теперь все изменилось, и мы открываем двери для международной аудитории. Первая мысль: "О, дерьмо. Нам нужно реорганизовать все наше решение!"
TimeZoneInfo
Мы открыли LinQPad и начали рисовать различные конвертеры для преобразования регулярных объектов DateTime
в объекты DateTimeOffset
на основе объекта TimeZoneInfo
, который был создан на основе значения User TimeZone User из указанного профиля пользователя.
Мы решили, что мы изменим все свойства DateTime
в моделях на DateTimeOffset
и покончим с этим. В конце концов, у нас появилась вся информация, необходимая для хранения и отображения локальной даты и времени пользователя.
Значительная часть фрагментов кода была вдохновлена сообщение в блоге Рика Страгла по этому вопросу.
NodaTime и DateTimeOffset
Но затем я прочитал Отличный комментарий Мэтта Джонсона. Он подтвердил мое намерение перейти на DateTimeOffset
, заявив: "DateTimeOffset имеет важное значение в веб-приложении".
Что касается времени Ноды, Мэтт говорит:
Говоря об Noda Time, я не соглашусь с вами, что вам нужно заменить все на всей вашей системе. Конечно, если вы это сделаете, у вас будет гораздо меньше возможностей совершать ошибки, но вы, безусловно, можете просто использовать Noda Time там, где это имеет смысл. Я лично работал над системами, которые необходимы для преобразования часовых поясов с использованием часовых поясов IANA (например, America/Los_Angeles), но отслеживал все остальное в типах DateTime и DateTimeOffset. На самом деле довольно часто можно увидеть, что Noda Time широко используется в логике приложений, но полностью покинул уровни DTO и persistence. В некоторых технологиях, таких как Entity Framework, вы не могли напрямую использовать Noda Time, если хотите - потому что там нет места для его подключения.
Это могло быть прямо направлено на нас, поскольку мы находимся в этом точном сценарии прямо сейчас, включая наш выбор для использования часовых поясов IANA.
Наш план, хороший или плохой?
Наша главная цель - создать наименее сложный рабочий процесс для работы с датами и временем в разных часовых поясах. Избегайте расчетов часового пояса как можно больше в наших Сервисах, Хранилищах и Контроллерах.
Вкратце, план состоит в том, чтобы принимать локальные даты и время из нашего front-end, как можно скорее конвертировать их в ZonedDateTime
и конвертировать их в DateTimeOffset
как можно раньше, прежде чем сохранять информацию в базы данных.
Ключевым фактором при определении правильного ZonedDateTime
является свойство TimeZoneId
в модели пользователя.
public class ApplicationUser : IdentityUser
{
[Required]
public string TimezoneId { get; set; }
}
Локальная дата-время до NodaTime
Чтобы предотвратить многократный код, наш план состоит в создании пользовательских ModelBinders, которые преобразуют локальные 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 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 упрощает выполнение специальных вычислений и запросов с часовым поясом.
Это правильное предположение?