Отправка DateTimeOffset через HttpClient C#
Столкнулся с проблемой в передачи времени на сервер написанный на WebApi. Подозреваю, что некорректно данные сериализуются в Json формат. Данные передаю в формате DateTimeOffset, клиент — простая форма на WPF. Модель данных на клиенте (на сервере аналогичная, только с атрибутами для Id):
public class Person < public int Id < get; set >public string Name < get; set; >public string LastName < get; set; >public string Department < get; set; >public DateTimeOffset PhotoUploadDateTime < get; set; >public string Photo < get; set; >>
Метод отправки
private void AddUserClick(object sender, RoutedEventArgs e) < var fileModel = new Person < //Id = new Guid(), Name = txtName.Text, LastName = txtLastName.Text, Department = txtDepartment.Text, PhotoUploadDateTime = DateTimeOffset.UtcNow, Photo = Convert.ToBase64String(image) >; var client = new WebApiSender(urlAddress); client.AddPerson(fileModel); MessageBox.Show("File has been uploaded"); >
Метод AddPerson HttpClient’а:
public async Task AddPerson(Person person) < using (var client = new HttpClient()) < client.BaseAddress = new Uri(baseAddress); client.DefaultRequestHeaders.Accept.Clear(); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var response = await client.PostAsJsonAsync("api/employee/", person); if (!response.IsSuccessStatusCode) < throw new Exception("Error when adding file!"); >> >
Заполнение модели данных на клиенте
Данные которые пришли в контроллер и записываются в БД
В итоге на сервер в контроллер приходят валидные значения, кроме времени и даты.
Маленькие чудеса C#/.NET – структура DateTimeOffset
Рассмотрим некоторые части .Net Framework’a, выглядящие тривиальными, но вполне способными сделать ваш код более простым как в написании, так и в сопровождении.
Пишущие на .NET (а если вы этого не делаете, то зря читаете этот пост) наверняка время от времени используют для своих нужд структуру DateTime. Эта структура удобна для хранения дат, времени или даты/времени, относящихся к локальной временной зоне (или же к UTC).
Однако, бывают случаи, когда вам необходимо сохранить время в виде смещения, а не конвертировать его в локальное время. И вот здесь вам на помощь придёт структура, впервые появившаяся в .NET 3.5 — DateTimeOffset.
Проблема: парсинг DateTime может привести к конвертации в локальное время
Представим себе, что вы используете файл, веб-сервис и т.п. некой сторонней фирмы, чьи сервера находятся в другой временной зоне. Более того, у них есть несколько полей, в возвращаемых данных, которые должны содержать даты, но на самом деле содержат сериализованные экземпляры структуры DateTime, время в которых установлено в полночь. Например, дату рождения пациента они передают вот в таком виде:
2012-03-01 00:00:00-05:00
Такая запись говорит о том, что человек родился 1 марта 2012 года в неуказанное время (в конце концов, большая часть форм не требует от вас заполнения времени вашего рождения). Но поскольку экземпляр структуры DateTime был сериализован «в лоб», то он и содержит время, установленное в полночь, согласно своей временной зоне.
Итак, зная, что эта дата совместима с Восточной временной зоной (Eastern Time Zone), а мы находимся в Центральной временной зоне (Central Time Zone) мы парсим её так:
// ясно что здесь выполняется чтение файла/потока/и т.п.
var dateString = «2012-03-01 00:00:00-05:00»;
// парсим в DateTime
var birthDay = DateTime.Parse(dateString);
Выглядит идеально, не так ли? Но тут кроется проблемка. Если мы проверим содержимое объекта DateTime на нашей локальной машине, где выставлена Центральная временная зона, то увидим вот что:
2012-02-29 11:00:00 PM
Что случилось? (Или как говорил один персонаж — Кто это сделал?) Да, метод DateTime.Parse() конвертировал дату в локальную временную зону поскольку оригинальная дата рождения хранилась с указанным смещением. Вам просто оказали услугу — конвертировали указанную дату и время в ваши локальные дату и время. Это не так и плохо, если бы речь не шла о дне рождения, которое с 1 марта переместилось на 29 февраля.
Конечно, мы можем созвониться с третьей стороной и попросить перестать включать время в строку с датой или же перестать высылать смещение вместе со временем (в этом случае она перестанет конвертироваться в локальное время, но будет отмечена как DateTimeKind.Unspecified).
Однако бывает, что у нас нет возможности таким образом изменить ситуацию.
Бывают случаи, когда вы хотите считывать дату и время со смещением, но не конвертировать его в локальную временную зону. И вот тут вам пригодится DateTimeOffset.
DateTimeOffset – хранит DateTime и Offset
Так что там про DateTimeOffset? Структура так же проста, как и её имя, DateTimeOffset это дата+время+смещение. Именно поэтому она представляет намного более точную точку во времени, поскольку включает информацию о смещении, по которому были установлены текущие дата и время.
По правде говоря, функциональность DateTime и DateTimeOffset во многом перекрывается, а поскольку у Microsoft есть руководство по выбору того или другого, то я рекомендую ознакомиться с ней в MSDN. Статья называется «Choosing Between DateTime, DateTimeOffset, and TimeZoneInfo».
В целом, вы можете использовать DateTime, если вы «прикреплены» к одной временной зоне или используете только универсальное время в формате UTC. Но если вы хотите использовать даты и время из разных временных зон, а также хотите сохранить информацию о смещении без конвертации в локальное время, то лучше использовать DateTimeOffset.
В структуре DateTimeOffset есть много таких же свойств, как и в структуре DateTime (Day, Month, Year, Hour, Minute, Second, и т.п.), потому здесь их я описывать не стану. Главное отличие состоит в нескольких новых свойствах:
Возвращает DateTime без учёта смещения.
LocalDateTime
Возвращает конвертированный DateTime, с учётом смещения (т.е. в локальной временной зоне).
Возвращает смещение относительно UTC.
UtcDateTime
Возвращает DateTime как время UTC.
Свойство DateTime возвращает вам DateTime (не приведенное к локальной временной зоне), а свойство Offset имеет формат TimeSpan, представляющее смещение от времени UTC. Также есть свойства LocalDateTime и UtcDateTime, конвертирующие данный DateTimeOffset в DateTime для локальной временной зоны или UTC.
Также замечу, что свойства Now и UtcNow структуры DateTimeOffset возвращают не тип DateTime, а DateTimeOffsets с соответствующим смещением от UTC. Конечно, как и DateTime, DateTimeOffset обладает методами, оперирующими с датой/временем, возвращая тип DateTimeOffset вместо DateTime.
Так как нам всё это поможет в выше приведенном примере? Теперь мы знаем, что третья сторона высылает нам дату и время своей временной зоны, которое не нужно конвертировать в локальные дату/время. Поэтому можно использовать DateTimeOffset.Parse() (или TryParse()) и выбрать только дату:
// ясно что здесь выполняется чтение файла/потока/и т.п.
var dateString = «2012-03-01 00:00:00-05:00»;
// парсим день рождения как смещение даты/времени (без конвертирования в локальные даты/время)
var dtOffset = DateTimeOffset.Parse(dateString);
// теперь если нам надо сравнить результат с другими объектами типа локального DateTime
// мы просто используем свойство Date для получения даты
// без времени или смещения
var theDay = dtOffset.Date;
Таким образом, вы можете легко парсить даты без необходимости отслеживания “полночного сдвига” или же использовать его там, где необходимо иметь дату, время и смещение без конвертирования в локальное время.
Хоть структура DateTime и является достаточно мощной в плане парсинга, манипулирования и сравнения дат/времени, она может доставить немало неприятных минут в работе с датами в формате разных временных зон. DateTimeOffset в этом случае проявляет себя куда более гибкой, поскольку использует смещение от UTC.
Вольный перевод (с) В.Ф.Чужа ака hDrummer, оригинал здесь.
Пишем тесты с временем в .NET 8+
Рассказываем, как тестировать на .NET 8 с использованием временных абстракций в виде TimeProvider и ITimer.
В .NET 8 представили абстракции улучшающие работу со временем.
Немного истории
DateTime
Основной структурой хранения даты и времени является DateTime, которая появилась в одном из первых релизов .NET — версии 1.1.
У структуры стуществует главный недостаток — отсутсвие временной зоны. Для обхода такого неприятного момента к DateTime добавили поле Kind со значениями: Local , Utc , или Unspecified . Так, можно получить местное время вызвав DateTime.Now , где Kind будет равен Local . А для перевода в UTC можно воспользоваться вызовом DateTime.Now.ToUniversalTime() , или что более просто, сразу вернуть DateTime.UtcNow .
Как же .NET понимает временную зону при переводе из местного времени в UTC? Метод ToUniversalTime берет временную зону из операционной системы. Тогда, если создать на сервере Нью-Йорка экземпляр DateTime.Now , отправить его в Лондон, а потом на обеих машинах вызвать перевод в UTC, то результат будет разным.
В Microsoft выпустили рекомендацию по этому поводу, переложив всю ответственность на разработчиков:
Разработчик несет ответственность за отслеживание информации о часовом поясе, связанной со значением DateTime через внешние механизмы.
Иными словами сам разработчик должен хранить информацию о часовом поясе, дополнительно к DateTime .
Альтернативным решением может быть хранение времени только в формате UTC, и последующая конвертация к местному времени на стороне пользователя. К сожалению, это требует дополнительных проверок кода против случайного использования DateTime с локальным временем, а также не исключает таких нюансов, как изменения правил перехода на зимнее время.
DateTimeOffset
В качестве улучшения в .NET 2 появилась структура DateTimeOffset, которая состоит из:
- структуры DateTime, и
- свойства .Offset — разницы во времени по отношению к UTC.
Описанной проблемы с серверами в разных временных зонах с DateTimeOffset.Now уже не произойдет.
Но и это не панацея от всех случаев. Допустим в Лондоне для пользователя необходимо сохранить запись к врачу на апрель. Добавляем запись c TimeZoneInfo.FindSystemTimeZoneById(«GMT Standard Time») , и .NET корректно посчитает время DateTimeOffset = 2023-04-01 14:00:00 с Offset = +1 . В конце марта в Британии происходит переход на летнее время и к Offset добавляется +1 час, в а в началае марте Offset = 0 . В некторых странах нет перехода на летнее время, например, в Нигерии Offset всегда равен +1 . Вроде никаких проблем, но иногда правила перехода меняются, как это было недавно в Европейском союзе. Получается, для клиентов желательно хранить и зону времени с помощью типа TimeZoneInfo для возможного перерасчета Offset по новым правилам.
Тестирование до .NET 8
Для Unit тестирования важно иметь возможность заменить вызов функции у объекта на свой собственный. Для этого используются Mock’и или Stub’и над абстракциями. Так как DateTime и DateTimeOffset не предоставляют инетерфейсов, то можно создать свой, передавать его тип в качестве аргумента функциям, и далее делать Mock’и при тестировании. Например, можно создать такой:
public interface ISystemClock < DateTimeOffset UtcNow < get; >>
Аналогичную работу сделали в Microsoft, добавив тот же самый код по меньшей мере в 4 разные области .NET.
Какие еще есть решения
Jon Skeet создал библиотеку NodaTime с правильным расчетом времени в самых тонких моментах, и конечно, с поддержкой абстракций.
Хорошо, используем свою или стороннюю абстракцию над временем .NET. А как же проводить интеграционные тесты с внешними библиотеками, которые все еще требуют передачи данных типа DateTime / DateTimeOffset ?
В Microsoft существует инструмент под названием .NET Fakes. Он генерирует Fake’и для любой .NET библиотеки. Например, можно перезаписать статический вызов DateTime.Now в тестах:
System.Fakes.ShimDateTime.NowGet = () => < return new DateTime(2025, 12, 31); >;
Работает, но есть ограничения. Первое, генератор совместим только с Windows. Второе, генератор включен только в дорогую версию Visual Studio Enterpise. Так, несколько лет назад одной крупной компании пришлось купить лицензию Enterpise всей команде разработчиков и авто-тестировщиков только для покрытия кода тестами с использованием .NET Fakes. Больше никаких возможностей из обширного набора Enterprise версии не использовалось. Браво отделу продаж Microsoft!
Тестируем с .NET 8
После многолетних дебатов и сотней комментариев в .NET 8 RC добавили долгожданные абстракции времени в виде TimeProvider и ITimer :
public abstract class TimeProvider < public static TimeProvider System < get; >protected TimeProvider() public virtual DateTimeOffset GetUtcNow() public DateTimeOffset GetLocalNow() public virtual TimeZoneInfo LocalTimeZone < get; >public virtual long TimestampFrequency < get; >public virtual long GetTimestamp() public TimeSpan GetElapsedTime(long startingTimestamp) public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) public virtual ITimer CreateTimer(TimerCallback callback, object? state,TimeSpan dueTime, TimeSpan period) > public interface ITimer : IDisposable, IAsyncDisposable
В конце концов, мы ожидаем, что почти никто не будет использовать что-либо кроме TimeProvider.System в эксплуатации. В отличие от многих абстракций, эта особенная: она существует исключительно для тестируемости.
Stephen Toub Microsoft
Получилось все же не идеально, но и это большой прогресс.
Ложки дегтя
1. Абстрактный класс вышел громоздким. Если у вас есть метод принимающий в качестве аргумента TimeProvider , без знания деталей метода нет возможности понять нужно ли делать Mock для GetUtcNow() или GetLocalNow() , или даже CreateTimer(. ) при тестировании. Что же в итоге будет вызвано? Разработчики предлагали разбить новый тип на небольшие интерфейсы, в частности аналогичный уже используемому внутри самого .NET:
public interface ISystemClock < public DateTimeOffset GetUtcNow(); >public abstract class TimeProvider: ISystemClock < // . >
Но от этой идеи в Microsoft отказались. Причина: идея хранения связанной логики времени в одном месте, даже если испоьзуются только ее небольшие части.
2. Добавили статическое свойство TimeProvider.System , которое возвращает системный экземпляр TimeProvider .
Пользователям очень легко использовать такой код, но это ничем не отличается от старого применения статического DateTime.Now . И так просто без FakeTimeProvider протестировать код не получится.
Вместо прямого вызова статического экземпляра, ожидается что программисты будут сразу использовать Dipendency Injection:
public class MyService < public readonly TimeProvider _timeProvider; public MyService(String timeProvider)< _timeProvider = timeProvider; >public boolean IsMonday() < // использование _timeProvider.GetLocalNow() >>
и далее в ASP.NET Core:
var builder = WebApplication.CreateBuilder(); // по умолчанию добавляем системный экземпляр, а в тестах его перезапишем builder.Services.AddSingleton(TimeProvider.System); builder.Services.AddSingleton()
Для новичков это может быть совсем не тривиально.
Хорошие новости
1. Тестирование времени становится более универсальным. Например, можно сделать Mock метода TimeProvider.GetUtcNow :
using Moq; using NUnit.Framework; [Test] public void MyTest() < var mock = new Mock(); mock.Setup(x =>x.GetLocalNow()).Returns(new DateTimeOffset(2025, 12, 31, 23, 59, 59, TimeSpan.Zero)); var mockedTimeProvider = mock.Object; var myService = new MyService(mockedTimeProvider); var result = myService.IsMonday(mockedTimeProvider); Assert.IsTrue(result, "Should be Monday"); >
2. Команда Mircosoft не стала привносить старую ошибку когда для сторонних эффектов использовались свойства вместо функций. Так ошибочно реализованы DateTime.Now . При обработке времени TimeProvider использует только методы: GetUtcNow() , GetLocalNow() , GetTimestamp() , и т.д.
3. Добавили возможность тестировать события таймера через TimeProvider.CreateTimer(. ) и ITimer.Change(. ) . Особенно это актуально для функций Task.Delay(. ) и Task.WaitAsync(. ) , которые также перевели на использование нового TimeProvider . Разработчики сторонних библиотек хорошо приветствовали на эту новость.
4. Обещают сделать встроенный в .NET класс FakeTimeProvider для еще большего упрощения тестирования. Тогда возможно отрицательный пункт 2 не будет актуален.
Выводы
Новый класс TimeProvider предоставляет унифицированную абстракцию времени с возможностью тестирования в экосистеме .NET, пусть и с некоторыми недостатками. Разработчики Microsoft пометили свои предыдущие наработки как устаревшие, и рекомендуют переводить код на TimeProvider .
Работа с датами и временем
Для работы с датами и временем в .NET предназначена структура DateTime . Она представляет дату и время от 00:00:00 1 января 0001 года до 23:59:59 31 декабря 9999 года.
Для создания нового объекта DateTime также можно использовать конструктор. Пустой конструктор создает начальную дату:
DateTime dateTime = new DateTime(); Console.WriteLine(dateTime); // 01.01.0001 0:00:00
То есть мы получим минимально возможное значение, которое также можно получить следующим образом:
Console.WriteLine(DateTime.MinValue);
Чтобы задать конкретную дату, нужно использовать один из конструкторов, принимающих параметры:
DateTime date1 = new DateTime(2015, 7, 20); // год - месяц - день Console.WriteLine(date1); // 20.07.2015 0:00:00
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // год - месяц - день - час - минута - секунда Console.WriteLine(date1); // 20.07.2015 18:30:25
Если необходимо получить текущую время и дату, то можно использовать ряд свойств DateTime:
Console.WriteLine(DateTime.Now); Console.WriteLine(DateTime.UtcNow); Console.WriteLine(DateTime.Today);
20.07.2015 11:43:33 20.07.2015 8:43:33 20.07.2015 0:00:00
Свойство DateTime.Now берет текущую дату и время компьютера, DateTime.UtcNow — дата и время относительно времени по Гринвичу (GMT) и DateTime.Today — только текущая дата.
При работе с датами надо учитывать, что по умолчанию для представления дат применяется григорианский календарь. Но что будет, если мы захотим получить день недели для 5 октября 1582 года:
DateTime someDate = new DateTime(1582, 10, 5); Console.WriteLine(someDate.DayOfWeek);
Консоль высветит значение Tuesday, то есть вторник. Однако, как может быть известно из истории, впервые переход с юлианского календаря на григорианский состоялся в октябре 1582 года. Тогда после даты 4 октября (четверг) (еще по юлианскому календарю) сразу перешли к 15 октября (пятница)(уже по григорианскому календарю). Таким образом, фактически выкинули 10 дней. То есть после 4 октября шло 15 октября.
В большинстве случаев данный факт вряд ли как-то повлияет на вычисления, однако при работе с очень давними датами данный аспект следует учитывать.
Операции с DateTime
Основные операции со структурой DateTime связаны со сложением или вычитанием дат. Например, надо к некоторой дате прибавить или, наоборот, отнять несколько дней.
Для добавления дат используется ряд методов:
- Add(TimeSpan value) : добавляет к дате значение TimeSpan
- AddDays(double value) : добавляет к текущей дате несколько дней
- AddHours(double value) : добавляет к текущей дате несколько часов
- AddMinutes(double value) : добавляет к текущей дате несколько минут
- AddMonths(int value) : добавляет к текущей дате несколько месяцев
- AddYears(int value) : добавляет к текущей дате несколько лет
Например, добавим к некоторой дате 3 часа:
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // 20.07.2015 18:30:25 Console.WriteLine(date1.AddHours(3)); // 20.07.2015 21:30:25
Для вычитания дат используется метод Subtract(DateTime date) :
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // 20.07.2015 18:30:25 DateTime date2 = new DateTime(2015, 7, 20, 15, 30, 25); // 20.07.2015 15:30:25 Console.WriteLine(date1.Subtract(date2)); // 03:00:00
Здесь даты различаются на три часа, поэтому результатом будет дата «03:00:00».
Метод Substract не имеет возможностей для отдельного вычитания дней, часов и так далее. Но это и не надо, так как мы можем передавать в метод AddDays() и другие методы добавления отрицательные значения:
// вычтем три часа DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); // 20.07.2015 18:30:25 Console.WriteLine(date1.AddHours(-3)); // 20.07.2015 15:30:25
Кроме операций сложения и вычитания еще есть ряд методов форматирования дат:
DateTime date1 = new DateTime(2015, 7, 20, 18, 30, 25); Console.WriteLine(date1.ToLocalTime()); // 20.07.2015 21:30:25 Console.WriteLine(date1.ToUniversalTime()); // 20.07.2015 15:30:25 Console.WriteLine(date1.ToLongDateString()); // 20 июля 2015 г. Console.WriteLine(date1.ToShortDateString()); // 20.07.2015 Console.WriteLine(date1.ToLongTimeString()); // 18:30:25 Console.WriteLine(date1.ToShortTimeString()); // 18:30
Метод ToLocalTime() преобразует время UTC в локальное время, добавляя смещение относительно времени по Гринвичу. Метод ToUniversalTime() , наоборот, преобразует локальное время во время UTC, то есть вычитает смещение относительно времени по Гринвичу. Остальные методы преобразуют дату к определенному формату.