Не зависимый от локали парсинг даты из строки в DateTime

В одном из проектов, волею судеб у меня приключилось что дата приходит из сторонней системы в виде строки формата Y-m-d и эту дату нужно сложить в столбец типа date в MS SQL сервере. В самой программе осуществляется запрос INSERT с этими данными. И одна ленивая задница (конечно это был я) решила не преобразовывать данные внутри программы, а просто сунуть их в параметр запроса. И вот что из этого получилось.

А получилось как всегда приключения в продуктиве. Через несколько лет эксплуатации программы появился клиент у которого настройки MS SQL подразумевали форматирование даты в виде Y-d-m, то есть день месяца теперь в середине строки, а не в конце. Маковкой на тортике было условие, что нельзя изменять настройки у сервера. То есть жить с тем, что дадено.

Возможные варианты конвертирования даты в MS SQL

После консультации с гуру MS SQL мне были предложены варианты решения проблемы:

  1. При начале сессии изменять формат строки даты по-умолчанию с помощью команды SET DATEFORMAT https://docs.microsoft.com/ru-ru/sql/t-sql/statements/set-dateformat-transact-sql?view=sql-server-ver15. Вариант подкупает своей простотой. Но уж очень он похож на костыль.
  2. Изменить саму строку для передачи в запрос. Вариант упоротый, конечно. Получить значение текущего формата даты MS SQL сервера можно с помощью запроса select date_format from sys.dm_exec_sessions where session_id = @@spid . Делать так мы, конечно, не будем.
  3. Прицеплять к параметру запроса нормальный тип DateTime и пусть драйвер ODBC MS SQL делает свою работу какой бы ни был в последствии формат даты. Вот это вариант хорош и закрывает технический долг который я сам себе сделал.

Выбор места в коде для конвертирования

Для задания параметров запроса в проекте я использую метод AddWithValue

В переменной serv у нас data class в котором находятся параметры из другой части программы и где мы получаем в поле Birthday нашу строку с датой.

Возникает логичный вопрос: а где правильно будет поменять этот параметр? В data class или в момент подготовки запроса?

Самый-самый правильный вариант: конечно, в data class. То есть мы должны в момент получения данных преобразовать строку в тип DateTime и уже внутри программы манипулировать ей.

Другой вариант. Взвешивания «за» и «против». Юнит тестами не получится протестировать все кейсы взаимодействия с data class-ом, нужно поднимать среду интеграционного тестировать и UAC + подключать тестировщиков. Что выйдет в очень дорого и долго. Если изменить только перед конкретным применением в запросе, то можно будет быстро исправить кейс и утилита будет приносить счастье пользователям.

Да, в общем, я выбрал второй вариант ибо бюджеты, сроки, и всё такое. Ну и в который раз убедился, что срезав углы при разработке, получаем эти углы в виде кочек при эксплуатации.

Конвертирование строки в DateTime в C#

Отбросив лирику, приступим к реализации. Нам дан формат строки Y-m-d и нам нужно его вставить в столбец DateTime в MS SQL.

Сходу могу предложить два варианта:

  1. Использовать метод класса Convert.ToDateTime
  2. Использовать метод класса DateTime.Parse

Для начала, проведём исследование через юнит-тестирование как эти методы помогут нам в решении задачи. Все исходные коды собраны в проект и расположены на GitHub https://github.com/a13xg0/str_to_date_csharp.

Тест для проверки нашего условия довольно прост и включает в себя дату, выходящую за пределы количества месяцев:

После того, как проверим что тест успешно упал на возврате из метода DateTime.Now приступим к проверке кода:

Тест успешно пройден. Но какие подводные камни нас могут подстерегать? Меня смущает тот факт, что форматов даты много, но как класс Convert угадал наш вариант? Не поймаем ли мы следующий баг на настройках компьютера под какой-нибудь другой локалью?

Идём курить мануалы: https://docs.microsoft.com/en-us/dotnet/api/system.convert.todatetime?view=net-5.0#System_Convert_ToDateTime_System_String_ и видим, что да:

If value is not null, the return value is the result of invoking the DateTime.Parse method on value using the formatting information in a DateTimeFormatInfo object that is initialized for the current culture.

Никакого угадывания, а чёткий посыл к настройкам системы.

Конвертирование без учёта системной локали

И ещё, здесь же мы видим, что внутри происходит обращение к методу DateTime.Parse, так что мы не будем плодить обёрток вызовов и продолжим эксперименты над DateTime.

Наш тест практически не отличается. Убеждаемся что он успешно падает и преобразуем дату сначала в лоб:

И да, это работает, как и ожидалось. Теперь проверим как это работает с другими настройками системной локали. Для этого проверим работу с форматом в виде точек.

И код, который мы проверяем:

Итоговый код конвертирования строки в дату

Оба теста проходят успешно. То есть, мы убедились что прежний (системный) формат даты перестал работать, а наш новый формат успешно и корректно распознаётся. Значит этот код с ожидаемым форматом ‘yyyy-MM-dd’ мы и будем использовать для преобразования из строки в дату.

Подытожив, можно сказать:

  1. Технический долг приходится платить всегда. Или вам или вашим пользователям
  2. Ожидайте неожиданного, полагаться на значения по умолчанию не нужно.
  3. Лучший способ парсить дату в не зависимости от настроек это использовать DateTime.ParseExact с настройками шаблона даты который ожидается увидеть.

Код проекта с тестами доступен https://github.com/a13xg0/str_to_date_csharp