Представим, что вы хотите преобразовать свою систему из одного состояния в другое. Начальное состояние — это когда DateTime используется везде, в C# коде и в БД. Конечное состояние — когда везде используется DateTimeOffset. Вы хотите сделать переход плавно и внести как можно меньше изменений. Это описание может быть началом очень интересной задачи с тупиком в конце.
Тип DateTime был типом .NET по умолчанию для работы с датой и временем некоторое время назад, и обычно построенная вокруг него логика была сделана так, как будто она никогда не поменяется. Если попытаться менять тип в один этап, это приведёт к каскадным изменениям практически во всех частях системы. В крайних случаях может потребоваться изменить около 200 хранимых процедур только для одного поля. Это первая проблема. И вторая проблема заключается в том, что последствия таких изменений трудно найти во время тестирования. Регрессионное тестирование не гарантирует, что вы ничего не упустите, или система будет функционировать в любых случаях. Необходимые усилия по обеспечению качества будут увеличиваться по мере работы, и у вас не будет четкого понимания, когда оно закончится.
Во время моего исследования я нашел возможный подход к таким преобразованиям. Этот подход имеет три этапа и основан на предположении, что в настоящее время система не поддерживает временные зоны, а все подсистемы расположены в одном часовом поясе.
Такой подход поможет локализовать изменения и ограничить усилия QA. Он также обеспечит хорошую предсказуемость для оценки будущей работы. Ниже я описал этапы более подробно.
Представьте, что существует около 150 полей, связанных со значениями даты/времени. Вы можете использовать следующий сценарий SQL, чтобы узнать полный список в вашей БД.
В то время как в БД преобразование из DateTime в DateTimeOffset и обратно поддерживается на очень хорошем уровне, в C# коде это сложно из-за типизации. Вы не можете прочитать значение DateTime, если БД возвращает значение DateTimeOffset. При изменении возвращаемого типа для одного поля необходимо изменить все места, где он используется во всей системе. В некоторых случаях это просто невозможно, потому что вы можете не знать о некоторых местах, если система очень большая. Именно по этой причине подход с простым изменением типа поля не будет работать. Вы можете попробовать найти все использования определенного поля таблицы в БД, используя следующий скрипт.
Для того, чтобы сделать трансформацию, важно заранее предсказать, какие части системы будут затронуты. У вас должен быть подход к локализации изменений в конкретном модуле системы без нарушения остальных частей.
Это подход просто «получше», а не лучший. Я все еще ожидаю, что некоторые проблемы могут появиться в будущем, но он выглядит более безопасным, чем предыдущий. Главное отличие состоит в том, что вы не выполняете преобразование за один шаг. Существует последовательность зависимых изменений, которая даст вам контроль над ситуацией.
Когда вы добавляете вычисляемое дублирующее поле в БД, вы вводите новое поле с требуемым типом. Это позволит вам отделить чтение, запись и отделить обновленный код от старого. Эта операция легко может быть выполнена с помощью скрипта, и никаких усилий по обеспечению качества не требуется.
Основываясь на приведенном выше результате, вы можете нарезать свою систему на разделы, где вы хотите ввести DateTimeOffset. Теперь вы можете использовать новый тип только в одной хранимой процедуре без необходимости изменять все связанные с ним места.
Операции чтения оказались наиболее сложными для преобразования из-за подхода, который используется для интеграции между клиентским кодом и БД. Значения даты/времени передаются посредством сериализации строк. DateTimeOffset имеет другой формат и не может быть прочитан по умолчанию для переменных DateTime на стороне клиента. В тоже время, операции записи просто работают. Если вы передадите значение DateTime аргументу или полю DateTimeOffset, это значение будет принято с предположением, что оно является скорректированным к UTC. Смещение времени после преобразования будет «+00:00».
Теперь можно взять какой-то раздел системы и определить точное количество хранимок, возвращающих DateTime в код клиента. Тут надо будет изменить операции чтения в коде C# для чтения значений DateTimeOffset. Также потребуется изменить сами хранимки в БД, чтобы они возвращали значения из новых вычисляемых полей. Ожидаемый результат этого шага выглядит следующим образом:
В итоге вы получите систему, которая считывает данные из новых полей, в то время, как сохраняет значения в старые. Теперь, как только смещение времени будет передаваться в операциях записи и сохраняться в системе, вся система начнет корректно работать с часовыми поясами.
Теперь надо фиксировать смещение времени в системе, отправлять его в БД и сохранять в полях. Надо взять старое поле и изменить его на вычисляемое из нового, а новое должно теперь содержать значения. Вы уже читаете их них, теперь еще и записываете значения, а старые — наоборот, только чтение. Этот подход поможет вам изолировать изменения только для определенного раздела. Ожидаемый результат выглядит следующим образом:
В итоге вы получите систему, которая записывает и читает новый тип DateTimeOffset. Этот тип имеет встроенную поддержку смещения по времени, поэтому вам не нужно будет делать какое-либо ручное преобразование к UTC или между временными зонами, в общем случае.
Единственную рекомендацию, которую я могу дать относительно деления системы на секции для преобразования следующая: необходимо предусмотреть достаточную изолированность модулей в соответствии с используемыми хранимками. Таким образом, вы добьетесь предсказуемости усилий и сможете их оценить заранее. Несомненно, какие-то проблемы все еще могут возникнуть, но они не будут расти, как снежный ком по мере работы. Позже вы сможете избавиться от старых полей. Информацию о тайм зоне можно брать из операционной системы или настроек пользователя. Ниже я разместил информацию о совместимости двух типов в БД.
Очень интересно услышать истории о подобной трансформации от тех, кто уже проделывал такой в своих системах. А так же, кто и как поддерживает таймзоны в своих системах.
Тип DateTime был типом .NET по умолчанию для работы с датой и временем некоторое время назад, и обычно построенная вокруг него логика была сделана так, как будто она никогда не поменяется. Если попытаться менять тип в один этап, это приведёт к каскадным изменениям практически во всех частях системы. В крайних случаях может потребоваться изменить около 200 хранимых процедур только для одного поля. Это первая проблема. И вторая проблема заключается в том, что последствия таких изменений трудно найти во время тестирования. Регрессионное тестирование не гарантирует, что вы ничего не упустите, или система будет функционировать в любых случаях. Необходимые усилия по обеспечению качества будут увеличиваться по мере работы, и у вас не будет четкого понимания, когда оно закончится.
Во время моего исследования я нашел возможный подход к таким преобразованиям. Этот подход имеет три этапа и основан на предположении, что в настоящее время система не поддерживает временные зоны, а все подсистемы расположены в одном часовом поясе.
- Добавить парное вычисляемое поле для чтения значений DateTimeOffset из БД.
- Сделать преобразование операций чтения.
- Сделать преобразование операций записи.
Такой подход поможет локализовать изменения и ограничить усилия QA. Он также обеспечит хорошую предсказуемость для оценки будущей работы. Ниже я описал этапы более подробно.
Неудачный Подход
Представьте, что существует около 150 полей, связанных со значениями даты/времени. Вы можете использовать следующий сценарий SQL, чтобы узнать полный список в вашей БД.
select
tbl.name as 'table',
col.name as 'column',
tp.name as 'type',
def.name as 'default'
from sys.columns col
inner join sys.tables tbl on tbl.[object_id] = col.[object_id]
inner join sys.types tp on tp.system_type_id = col.system_type_id
and tp.name in ('datetime', 'date', 'time', 'datetime2', 'datetimeoffset', 'smalldatetime')
left join sys.default_constraints def on def.parent_object_id = col.[object_id]
and def.parent_column_id = col.column_id
order by tbl.name, col.name
В то время как в БД преобразование из DateTime в DateTimeOffset и обратно поддерживается на очень хорошем уровне, в C# коде это сложно из-за типизации. Вы не можете прочитать значение DateTime, если БД возвращает значение DateTimeOffset. При изменении возвращаемого типа для одного поля необходимо изменить все места, где он используется во всей системе. В некоторых случаях это просто невозможно, потому что вы можете не знать о некоторых местах, если система очень большая. Именно по этой причине подход с простым изменением типа поля не будет работать. Вы можете попробовать найти все использования определенного поля таблицы в БД, используя следующий скрипт.
SELECT
ROUTINE_NAME,
ROUTINE_DEFINITION
FROM INFORMATION_SCHEMA.ROUTINES
WHERE ROUTINE_DEFINITION LIKE '%table%'
OR ROUTINE_DEFINITION LIKE '%field%'
AND ROUTINE_TYPE='PROCEDURE'
Для того, чтобы сделать трансформацию, важно заранее предсказать, какие части системы будут затронуты. У вас должен быть подход к локализации изменений в конкретном модуле системы без нарушения остальных частей.
Подход «Получше»
Это подход просто «получше», а не лучший. Я все еще ожидаю, что некоторые проблемы могут появиться в будущем, но он выглядит более безопасным, чем предыдущий. Главное отличие состоит в том, что вы не выполняете преобразование за один шаг. Существует последовательность зависимых изменений, которая даст вам контроль над ситуацией.
Создание Вычисляемого Поля
Когда вы добавляете вычисляемое дублирующее поле в БД, вы вводите новое поле с требуемым типом. Это позволит вам отделить чтение, запись и отделить обновленный код от старого. Эта операция легко может быть выполнена с помощью скрипта, и никаких усилий по обеспечению качества не требуется.
declare @table sysname, @column sysname, @type sysname, @default sysname
declare cols cursor for
select
tbl.name as 'table',
col.name as 'column',
tp.name as 'type',
def.name as 'default'
from sys.columns col
inner join sys.tables tbl on tbl.[object_id] = col.[object_id]
inner join sys.types tp on tp.system_type_id = col.system_type_id
and tp.name in ('datetime', 'date', 'time', 'datetime2', 'smalldatetime')
left join sys.default_constraints def on def.parent_object_id = col.[object_id]
and def.parent_column_id = col.column_id
order by tbl.name, col.name
open cols
fetch from cols into @table, @column, @type, @default
while @@FETCH_STATUS = 0
begin
declare @cmd nvarchar(max)
set @cmd = 'alter table ['+@table+'] add ['+@column+'_dto] as todatetimeoffset(['+@column+'], ''+00:00'')'
exec (@cmd)
fetch from cols into @table, @column, @type, @default
end
close cols
deallocate cols
Основываясь на приведенном выше результате, вы можете нарезать свою систему на разделы, где вы хотите ввести DateTimeOffset. Теперь вы можете использовать новый тип только в одной хранимой процедуре без необходимости изменять все связанные с ним места.
Трансформация Чтения
Операции чтения оказались наиболее сложными для преобразования из-за подхода, который используется для интеграции между клиентским кодом и БД. Значения даты/времени передаются посредством сериализации строк. DateTimeOffset имеет другой формат и не может быть прочитан по умолчанию для переменных DateTime на стороне клиента. В тоже время, операции записи просто работают. Если вы передадите значение DateTime аргументу или полю DateTimeOffset, это значение будет принято с предположением, что оно является скорректированным к UTC. Смещение времени после преобразования будет «+00:00».
Теперь можно взять какой-то раздел системы и определить точное количество хранимок, возвращающих DateTime в код клиента. Тут надо будет изменить операции чтения в коде C# для чтения значений DateTimeOffset. Также потребуется изменить сами хранимки в БД, чтобы они возвращали значения из новых вычисляемых полей. Ожидаемый результат этого шага выглядит следующим образом:
- C# код считывает DateTimeOffset и использует этот тип везде, где возможно, чтобы возвращать значения из системы.
- Хранимки БД используют DateTimeOffset в аргументах, и C# код передает им значение DateTimeOffset.
- Новый тип используется внутри хранимок БД.
- Хранимки БД возвращают значения из новых добавленных полей.
В итоге вы получите систему, которая считывает данные из новых полей, в то время, как сохраняет значения в старые. Теперь, как только смещение времени будет передаваться в операциях записи и сохраняться в системе, вся система начнет корректно работать с часовыми поясами.
Трансформация Записи
Теперь надо фиксировать смещение времени в системе, отправлять его в БД и сохранять в полях. Надо взять старое поле и изменить его на вычисляемое из нового, а новое должно теперь содержать значения. Вы уже читаете их них, теперь еще и записываете значения, а старые — наоборот, только чтение. Этот подход поможет вам изолировать изменения только для определенного раздела. Ожидаемый результат выглядит следующим образом:
- Код C# создает значения DateTimeOffset и передает их в БД
- Новые поля теперь являются реальными полями со значениями
- Cтарыt поля теперь вычисляемые и используются для чтения
- Хранимки БД сохраняют значения в новые поля
В итоге вы получите систему, которая записывает и читает новый тип DateTimeOffset. Этот тип имеет встроенную поддержку смещения по времени, поэтому вам не нужно будет делать какое-либо ручное преобразование к UTC или между временными зонами, в общем случае.
Дальнейшие Шаги
Единственную рекомендацию, которую я могу дать относительно деления системы на секции для преобразования следующая: необходимо предусмотреть достаточную изолированность модулей в соответствии с используемыми хранимками. Таким образом, вы добьетесь предсказуемости усилий и сможете их оценить заранее. Несомненно, какие-то проблемы все еще могут возникнуть, но они не будут расти, как снежный ком по мере работы. Позже вы сможете избавиться от старых полей. Информацию о тайм зоне можно брать из операционной системы или настроек пользователя. Ниже я разместил информацию о совместимости двух типов в БД.
- Изменение типа столбца из DateTime в DateTimeOffset работает с неявным преобразованием. Смещение времени будет +00:00. Если нужно указать другой часовой пояс, необходимо использовать временный столбец.
- Форматирование значений в строку поддержано.
- Сравнение всеми операторами поддержано.
- SYSDATETIMEOFFSET() без риска может заменить GETDATE()
- Любое присвоение между DateTime и DateTimeOffset работает с неявным преобразованием.
Операция | T-SQL | Комментарий |
---|---|---|
Конвертация DateTime в DateTimeOffset | TODATETIMEOFFSET(datetime_field, '+00:00') | Получите значение с добавленным смещением +00:00 |
Конвертация DateTimeOffset в DateTime | CONVERT(DATETIME, datetimeoffset_field) — or — SET @datetime = @datetimeoffset |
Информация о смещении будет потеряна. При преобразовании смещение будет просто проигнорировано. Например, для '2017-04-05 10:02:00 +01:00' получите '2017-04-05 10:02:00'. |
Текущее дата/время | SWITCHOFFSET(SYSDATETIMEOFFSET(), '+00:00') | Это две команды. Результатом будет точка в UTC зоне |
Встроенные операции | DATEPART, DATEDIFF, BETWEEN, <, >, =, etc. | DATEDIFF, BETWEEN и операции сравнения учитывают временное смещение, при этом DateTime значение представляется, как значение со смещением UTC |
Форматирование | CONVERT(NVARCHAR, datetimeoffset_field, 103) | Получите тот же результат что и для DateTime. |
Очень интересно услышать истории о подобной трансформации от тех, кто уже проделывал такой в своих системах. А так же, кто и как поддерживает таймзоны в своих системах.
Поделиться с друзьями
zodchiy
Вечная проблема DateTime в связке js (angular) <-> asp.net mvc <-> mssql (UTC в datetime), это невозможность точно сказать, какое время будет отображено у пользователя на странице. Чего только не перепробовали и сами форматировали, и использовали сторонние библиотеки, в итоге остановились на DateTimeOffset — и все заработало.
AliasVeter
Для нас сработал следующий подход:
С фронтенда отдавали объект date в UTC формате (использовали momentJs). В Mongo все даты хранили в UTC. Когда нужно было вернуть дату с сервера на клиент, отдавали ее в UTC, а на клиенте приводили к локальной таймзоне.
zodchiy
У нас так не получалось. Дата писалась в БД (своя зона) как GETUTCDATE(), вроде бы, что может случится? Но когда мы брали дату из БД как DateTime в бэкэнде (другая зона), и передавали во angularJS фронтэнде (другая зона), то получалось, что мы теряли точное время при преобразовании. А уж когда эта дата возвращалась как параметр обратно в БД (дали клиенту выбор из дат загрузки данных, как пример), то мы точно не попадали в ту дату, которая была в БД.
momentJs не спасал, точнее он спасал, но только до фронтэнда, для отображения, дальше DateTime по мере путешествия БД > бэкэнд > фронтэнд > бэкэнд > БД менялся и не соответствовал тому-же значению в БД.
Сделали так: из БД брали DateTimeOffset, отдавали его во фронт, там он превращался в объект с двумя полями, одно поле тот же offset (например "/Date(1487688845183)/") которое улетало обратно как параметр, а другое уже отображаемая дата.
DateTimeOffset после всех сериализаций и десериализаций не менялся, и точно соответствовал тому, что хранилось в БД.
Dywar
Рекомендованный подход — принимайте UTC, храните UTC.
Если форматировать не удобно, кидайте в миллисекундах (если в 1 секунду может произойти несколько действий, и нужно выяснить что было раньше).
Отличный ресурс https://currentmillis.com/
Dywar
Еще, 50 минута а далее.
HKA
В общем случае UTC — не панацея. К примеру, сотрудник, находящийся в командировке, открывая бэкенд своей компании, должен видеть события в локальном времении компании, а не в локальном своем. «Заседание начинается в 15:00, явка обязательна».
AliasVeter
Если сотрудник находится в другом часовом поясе, то он в любом случае не сможет лично присутствовать на собрании. А вот смотреть трансляцию по Skype вполне может. И логично отображать ему локальное время трансляции, а не серверное время UTC.
HKA
А через неделю? В моем проекте так и происходит, что привилегированные юзеры колесят по стране и сами создают и корректируют удаленные события — естественно, в локальном времени места события.
AliasVeter
Вот вы и сохраняете время в UTC, со смещением относительно часового пояса в котором событие было создано. Когда отдадите время на фронтенд, приводите его к локальному часовому поясу. Если локальный часовой пояс совпадает с тем, в котором событие было создано — Вы компенсируете сдвиг дат. Если нет, то клиент получит актуальное время (со смещением) в своем часовом поясе.
HKA
Вы не поняли. Клиент — админ, он сам создает событие, скажем, в 18:00 локального времени места события, т.к. это публичное время (проще говоря, расклеено на афишах). Он оперирует только этим временем вне зависимости от его текущего местоположения.
indestructable
Самый надежный и хороший подход — это хранить DateTimeOffset, и работать с ним как на сервере, так и на клиенте. Использовать свой формат сериализации (возможно, с фоллбэком в часовую зону по умолчанию, для старых клиентов).
Все остальное, включая хранение UTC, это полумера. UTC решает проблему сравнения дат, но не всегда помогает правильно их отображать.
Вообще, работа с датами — тема довольно неочевидная и с множеством подводных камней, особенно если в некоторых случаях нужны только даты без времени. Нужно всегда (как отметили уже выше) понимать, что именно нужно показать: дату события в часовом поясе пользователя, дату, как ее ввел пользователь или еще что-то.
Dywar
UTC норм, если придерживаться его на входе и на выходе. Задача правильного отображения передается клиенту, он сам должен знать где он находится, в Канаде, Африке или Австралии, и какое там смещение.
Выбор между типами DateTime, DateTimeOffset, TimeSpan и TimeZoneInfo
На правах личных предпочтений.
indestructable
UTC
DateTime
позволяет сравнивать даты из разных часовых поясов, и отображать даты в часовом поясе пользователя. Это дает общую относительную систему координат, и часто этого достаточно.Но иногда возникают задачи, когда требуется узнать дату и время в системе координат пользователя.
Например, иногда важна именно выбранная дата (без времени), и она может отличатся из-за разницы в часовых поясах. Здесь UTC не поможет, из него нельзя восстановить реально выбранное пользователем значение (без дополнительной информации о часовом поясе, скажем, из профиля).
ETman
Есть несколько случаев, в которых UTC не поможет, даже если все четко с его сохранением и выводом.
1) Если вы сохраняете информацию о будущем событии, которое должно произойти именно в 10 утра в Апреле через год во Владивостоке. Сервера все в Лондоне. В течение года правительство решает не переходить на летнее время. В этом случае поможет сохранение в локальном времени.
2) При переходах от зимнего к летнему и обратно у вас может теряться или дублироваться час в UTC. В этом случае помогает наличие информации о смещении времени.
Вообще, чтобы описать конкретный момент во времени необходимо три величины: дата/время + оффсет + название тайм зоны. Если меньше информации, то время приблизительное, как ни крути. Но для бизнес задач может быть достаточным только UTC, бесспортно. Поэтому необходимо смотреть конкретный случай.
radium
По первым двум параметрам вопросов нет, а зачем название тайм зоны?
Если углубится, то при высоких требованиях к расчётам разницы между двух дат, в этот список можно добавить и количество добавленных високосных секунд — на данный момент 27.
ETman
Да, относительно UTC вы правы. Я запутался.
На счет названия таймзоны. По моим расчетам она нужна именно, чтобы получить эти карты. Офсет в большинстве случаев не нужен, т.к. можно получать локальное из UTC + TimeZoneInfo. Но без офсета будет проблема с переводом в локальное время тех моментов, которые произошли в течение этого DST перехода. Как то так.
Bronx
информацию о будущем событии, которое должно произойти именно в 10 утра в Апреле через год во Владивостоке.
Эта информация — не дата, и не интервал, поэтому ни DateTime, ни DateTimeOffset не являются правильными типами для планируемых событий.
Во-первых, тут есть, как минимум, информация о месте события, которую принципиально нельзя кодировать таймзоной, если не хочется проблем в будущем.
Во-вторых, «через год» — это плохо определённое понятие. Например, если сегодня 28 февраля, то что такое «через год» — 27 февраля или 1 марта? То же самое с понятием «через месяц».
Так что, в вашем сценарии нужно хранить локацию (координаты или код), смещение времени от начала локальных суток, а та часть, что касается будущей даты, должна храниться в виде параметров для алгоритма вычисления этой даты, и их множество не сводится к абсолютной дате или к смещению. И это ещё не вспомнили про рекуррентные события.
ETman
Не понял из вашего объяснения, как хранение локальной даты/времени может не работать для определения точного локального времени, безотносительно всяких переводов и политических решений.
В своем примере я имел ввиду, что можно хранить только название таймзоны и date/time (локальный).
Bronx
Населённый пункт может переехать из одной таймзоны в другую, если границы зоны изменятся.
dotnetdonik
Рекомендую всегда брать datetimeoffset. В дотнете много неожиданностей можно отловить с datetime. Например приведение при десериализации. Причина в работе форматеров- если клиент пришлёт datetimeoffset, а параметр типа datetime, время преобразуется с учётом серверной таймзоны. В принципе не понимаю в какой ситуации это может быть ожидаемым поведением.
HKA
При сериализации DateTime неожиданностей не будет, если следовать требованию выставлять Kind в DateTimeKind.Utc.
Razbezhkin
Как определять часовой пояс пользователя? По заголовкам браузера, или просить пользователя указывать его в профиле? Что лучше? Если передавать UTC клиенту и там преобразовывать к локальному времени, то как: javascript или есть другие варианты? А если на сервере формировать, то без знания часового пояса не обойтись…
Bronx
Хранить в локальном времени можно лишь если выполняются всё нижеперечисленное:
* единственным и исключительным потребителем информации о времени является тот, кто эту информацию произвёл
* потребителю не нужна однозначность времени.
Т.е., фактически, единственный приемлемый сценарий для хранения локального времени — это хранение времени как строки, в точности как она была введена, без обработки, без использования в расчётах, без передачи этой информации другим клиентам. Только чтобы приватно посмотреть на экране или распечатать. Как только начинаются расчёты, появляются несколько пользователей (сервер, другие клиенты) — немедленно переходить к однозначному представлению в хранилище (т.е. UTC) и к конвертации в локальное время на клиентах (они свои часовые пояса знают лучше, чем сервер).
ETman
При расчете будущих событий UTC не поможет, т.к. события, обычно, должны происходить в какое-то точное локальное время.
Скажем, как бы вы решали такую проблему наименьшими усилиями: надо расчитать даты запуска джоба в 10 утра по каждой из нескольких локаций, которые находятся в разных часовых поясах?
По моим расчетам, можно и через UTC: расчитываем последовательность, переводим в локальную, чтобы проверить 10 часов это или нет, сохраняем обратно в UTC.
А пожно просто забить как 10 утра локально и скедулер уже будет вызывать, без конвертаций и расчетов.
В чем видите тут проблему?
Bronx
Задание, я так понимаю, выполняется на лондонском сервере, и должно отработать в 10 часов владивостокского времени, даже если Владик сейчас оффлайн и результатов не получит? Потому что если Владик онлайн, то у него может работать свой планировщик, который ровно в 10 местного времени будет делать запрос в Лондон и инициировать задачу на центральном сервере. Если Владик оффлайн, то зачем вообще нагружать сервер задачей, результатов которой тот в ближайшее время, возможно, не увидит?
Но, в принципе, если у вас ограниченное число хорошо известных локаций и нет проблем обновлять таймзоны по ним, то да, ваше решение тоже работает. Мои замечания больше относятся к системам, которая должны обслуживать неопределённое число клиентов в любой мыслимой и немыслимой зоне.
ETman
Как определять таймзону не является большой проблемой. Проблема — сохранение этой информации в цикле разных расчетов и чтений-записи данных. Если эта информация всегда будет с date/time, то проблем почти не будет.
hVostt
Только DateTimeOffset! Без вариантов. Никаких UTC, — это костыль.
sand14
дубль.
sand14
Нужно хранить именно DateTimeOffset, т.к. при приведении к UTC теряется информация, какое локальное время и смещение были в момент регистрации события.
А эта информация может быть важна — выше привели много примеров.
И кстати, если на машине, где регистрировалось событие, часовой пояс был выставлен неверно, то полная исходная информация поможет выявить ошибку.
Кроме того, зачем вообще терять часть информации на входе, если к UTC всегда можно привести позже, если, к примеру, в отчете нужно показывать время в UTC.
И также верно выше заметили, что если совсем по-хорошему, то кроме смещения нужно хранить и название (или код) тайм-зоны.