Веб-разработка на .NET // dotnet web dev
94 subscribers
1 photo
34 links
Разрабатываю на .NET. В этом канале пишу заметки о программировании в целом и разработке на дотнете в частности

Электропочта: [email protected]
Электросайт: boyarincev.net
Download Telegram
Статья с хорошим объяснением механизма работы джойнов. Если вы забыли, то джойн даёт в результате декартово произведение записей двух таблиц.
А пока читал вспомнил, что когда работал с NHibernate при джойне двух таблиц со связью один ко многие, часто сталкивался с проблемой появления дублирующихся записей в результате выборки. Интересно, можно ли такую аномалию получить при использовании Entity Framework.

#базыданных
Оказывается Entity Framework Core может использовать INotifyPropertyChanged интерфейс для того, чтобы напрямую узнавать о изменениях в моделях и не использовать снапшот данных для их поиска. Думаю, что это можно попробовать использовать в высокопроизводительных сценариях.

#entityframework
Статья с разбором продвинутых техник использования проекций в Entity Framework Core.
1. Как хранить проекции для их переиспользования.
2. Как хранить и использовать вложенные проекции для проекций.
3. И наконец самое интересное - способ создания вложенных проекций не для коллекций, а для одиночных сущностей (для вложенной проекции одиночной сущности нельзя вызвать Select, а значит и нельзя сделать проекцию.

#entityframework
Ну и наконец две статьи в которых рассказывается, как можно изменить генерацию SQL в Entity Framework Core.

1. Extending SQL Generation in Entity Framework Core
2. Реализуем свой оператор в Entity Framework Core

#entityframework
Поговорим про производительность Entity Framework.

Во-первых существует whitepaper по производительности EF, из которого можно узнать очень много грязных низкоуровневых подробностей работы EF, но он не обновлялся для EF Core, поэтому в чём-то не актуален. Хотя концептуально всё-равно полезен, если у вас возникнет необходимость супер-оптимизировать и скорость работы EF Core тоже:

Performance considerations for EF 4, 5, and 6

Но whitepaper никак не решает проблему того, что Entity Framework в принципе не приспособлен для массовых операций изменения данных - массовых Insert, Update и Delete. Тут проблема лежит в двух плоскостях - тормозит DbContext и тормозит сама база данных, так как EF производит все изменения отдельными sql запросами (хотя и в рамках одного физического запроса к базе данных).

Техники работы с контекстом, чтобы он меньше тормозил при Insert рассмотрены в этой статье Rick Strahl: Entity Framework and slow bulk INSERTs
И техника с пересозданием DbContext и "пакетным" сохранением описанная там, довольно эффективна, а в этом ответе на SO есть сравнение влияния размера пакетов на скорость сохранения, так чтобы вы могли выбрать оптимальный.

Для ускорения контекста при массовом обновлении и удалении, тоже есть "хаки":
1. Аттачить к контексту "болванки" (сущности, которые на загружались из БД и которых нет в контексте), а затем явно их помечать в контексте как обновлённые или удалённые (через DbContext.Entry), кое-что по этой теме можно найти здесь.
2. Также как при Insert пакетировать сохранение.
3. Также как при Insert отключать автоматический поиск изменений в контексте.

Но самый эффективный путь - это вообще отказаться от использования Entity Framework для массовых операций изменения данных и, например, напрямую использовать SQL.
(Если решите пойти этим путём, то загляните ещё в эту статью, в ней автор рассказывают свою идею как сделать такие SQL-запросы более типизированными.)

А для тех случаев, когда даже SQL тормозит, можно пойти дальше и использовать более производительные способы, которые предоставляет база данных.

Например, вот в этой статье, рассказано, как использовать специальные возможности MSSQL Server для ускорения Insert и Update на уровне базы данных: Entity Framework: повышаем производительность при сохранении данных в БД. Подобную возможность, кстати, предоставляет и PostgreSQL.

Ну и буквально на днях наткнулся на библиотеку Linq2Db у которой есть интеграция с Entity Framework и с помощью которой, судя по всему, без проблем можно будет делать массовый Insert, Update, Delete. Кстати, умеет использовать специальные высокопроизводительные операции специфичные для базы данных. Но предупреждаю, сам её ещё не использовал.

#entityframework
#entityframework

Не помню уже, как велики были возможности логирования в классическом Entity Framework, но у Entity Framework Core с этим точно всё в порядке, статья на эту тему: Настройка логирования в Entity Frmework Core
Про Entity Framework и DDD

Обычно так случалось, что если архитектура у нас по DDD, а проект более менее большой и серьёзный, то мы как правило разделяли доменную и дата модели на две отдельные, потому что Entity Framework в своих ранних версиях накладывал много ограничений и специфичных требований на модели, но в процессе взросления EF эти ограничения становились всё меньше и теперь уже Entity Framework Core позволяет в качестве и доменных и дата моделей использовать одну и ту же модель не идя на компромиссы. А как это делать можно прочитать в статьях Джули Лерман:

1. DDD-Friendlier EF Core 2.0
2. DDD-Friendlier EF Core 2.0, Part 2

Или посмотреть в её выступлении на NDC Conference: Mapping DDD Domain Models with EF Core 2.1

И ещё есть хорошая статья на хабре: Сущности в DDD-стиле с Entity Framework Core

Подход с объединением моделей проще, потому что при использовании отдельных моделей для домена и дата слоя необходимо решить проблему отслеживания изменений в доменных сущностях и правильного переноса этих изменений в дата модели, так чтобы Entity Framework смог сохранить всё корректно. Готовой статьи на эту тему дать не смогу, но, в принципе, эта проблема релевантна проблеме под названием "Работа с отсоединёнными сущностями" - в случае, когда доменные и дата модели разделены, доменные модели это по сути и есть отсоединённые сущности, так что идеи как решить проблему синхронизации доменных и дата моделей можно почерпнуть, изучая как решают проблему работы с отсоединёнными сущностями. А статья на эту тему, например, вот: Доступ к данным - Обработка состояния отсоединенных сущностей в EF всё той же Джули Лерман.

Ну и если говорить о DDD, то ещё можно затронуть тему Спецификаций и в статье ниже отличный пример того, как можно реализовать спецификации для использования в EF: EntityFramework: (анти)паттерн Repository

#entityframework #DDD
Прерывание выполнения потока в .NET Core

В .NET Core выпилили метод Thread.Abort() и это значит, что теперь невозможно насильственное прерывание выполнения потока извне этого потока средствами управляемого кода.

Так что все эти знания о нюансах использования Thread.Abort() и чудесных способностях выбираться из try/catch блоков исключения ThreadAbortException, которые мы заучивали в Рихтере, теперь бесполезны - сегодня выкидывание ThreadAbortException невозможно.

Цитата из документации:

Even though this type exists in .NET Core, since Abort is not supported, the common language runtime won’t ever throw ThreadAbortException.

В этом Issue на GitHub подробное обсуждение плюсов и минусов такого решения.

Если кратко о причинах, то это было сделано, потому что очень дорого писать библиотеки, которые гарантировали бы защиту от corrupted state всего процесса, то есть гарантировали бы безопасность дальнейшей работы процесса, в условиях, когда в любой момент выполнение потока может быть прервано - а именно такие библиотеки нужно было написать разработчикам .NET Core. Поэтому если вам нужно прерывать выполнение потока, то используйте CancellationToken, если это ваш код или код чужих разработчиков поддерживающий его, или запускайте код, который потребует прерывание выполнения потока в отдельном процессе и прекращайте его целиком.

Вот некоторые цитаты из этого обсуждения:

The key problem with thread abort is that it affects reliability of the whole stack. If you are using thread abort, all code (ie all libraries) running in the process have to be robust against being killed by thread abort at any point. It is extremely expensive to audit and write libraries with this constrain.

Try to review any code that is doing a more complex managed/unmanaged interop (e.g. sockets in CoreFX) and try to find places where inserting a thread abort exception would cause bad things to happen. I am sure that you are going to find many places where inserting thread abort would lead to hangs, crashes or data corruptions. And there will be a lot more that the tests would discover. People just do not naturally write code that is able to recover from being aborted at any point.

It is hard to tell where it is “safe” to insert the thread aborts. Libraries that need to be robust in presence of Thread.Abort need to be annotated for it, coded in a special way and stress tested.

We have tried to do this in .NET Framework: It was a full time job for several people to run a stress harness that inserted thread about at random points in .NET Framework, and file and fix bugs on the crashes, hangs and data corruptions that it hit. This was done only for a subset of .NET Framework that was usable in SQLCLR, and still it was never ending stream of issues.

Even with this effort, we often got a support escalation (from paid support) where people hit problems with Thread.Abort in production. Some of these issues require a very ugly hacks to workaround because of there was just no right fix to them. We had to resort to hacks like decoding assembly instructions and suppress or adjust thread abort behavior for particular instruction pattern that was known to hit the problem.

I am not even talking about larger .NET ecosystem - if you take a random NuGet package from
nuget.org, it is almost guaranteed that it has reliability bugs in the presence of thread abort.

Да и Джон Скит, Эрик Липперт и Джо Даффи, задолго до .NET Core тоже не рекомендовали использовать Thread.Abort()

#dotnetcore
Числа с плавающей точкой в дотнете

Хотел бы я сейчас в паре абзацев доступно рассказать о том, как устроены числа с плавающей точкой в дотнете, но тема слишком сложная, чтобы уложить её в пост в телеграме и одновременно важная, хоть какое-то понимание о ней должно быть у каждого дотнетчика. Нужно иметь представление и чем decimal отличается от double и какие ограничения у каждого типа данных существуют, поэтому буду давать ссылки.

Моя статья: Представление чисел с плавающей точкой в памяти в дотнете - в которой разбирается как Double хранится в памяти, подробнее остановивливаясь на нормализованных числах (поймёте после прочтения) и на том как самому посмотреть, что в реальности хранится в памяти, а также разбирается представление в памяти нескольких чисел.

У Джона Скита есть две небольшие статьи на эту тему, первая: Binary floating point and .NET - про double и float, вторая: Decimal floating point in .NET - про decimal.

Сам Скит рекомендует к прочтению эту статью: Floating Point in .NET part 1: Concepts and Formats - она более подробная и позволит уже более основательно разобраться в вопросе.

Ещё есть статья на русском: Взгляд со стороны: Стандарт IEEE754 - она в целом о стандарте хранения чисел IEEE754, статья основательная, но довольно абстрактная и её не назовёшь лёгким, доступным материалом.

Ну и чтобы закрепить знания можно посмотреть два выступления Андрея Акиньшина на дотнексте и подумать над задачками, которые он задаёт зрителям, после прочтения статей выше многие ответы на задачи понять будет проще:
Поговорим про Арифметику
Продолжаем говорить про Арифметику

#computerscience #floatingpointnumbers
На днях поймали deadlock на блокировках в PostgreSQL и оказалась полезной эта статья - взял из неё запрос, который в удобном виде показывает, какие запросы какими запросами заблокированы

[eve] sandbox=# SELECT
COALESCE(blockingl.relation::regclass::text,blockingl.locktype) as locked_item,
blockeda.pid AS blocked_pid, blockeda.query as blocked_query,
blockedl.mode as blocked_mode, blockinga.pid AS blocking_pid,
blockinga.query as blocking_query, blockingl.mode as blocking_mode
FROM pg_catalog.pg_locks blockedl
JOIN pg_stat_activity blockeda ON blockedl.pid = blockeda.pid
JOIN pg_catalog.pg_locks blockingl ON(
( (blockingl.transactionid=blockedl.transactionid) OR
(blockingl.relation=blockedl.relation AND blockingl.locktype=blockedl.locktype)
) AND blockedl.pid != blockingl.pid)
JOIN pg_stat_activity blockinga ON blockingl.pid = blockinga.pid
WHERE NOT blockedl.granted
AND blockinga.datname='sandbox';

Ну и вообще статья хорошая - по шагам от простого сложного и с понятными примерами даёт неплохой экскурс в блокировки.

#postgresql
С термином инкапсуляция всё не очень просто - само слово нам мало что о себе сообщает, а его интерпретация в головах разных программистов варьируется в гигантских пределах, хотя сама инкапсуляция считается фундаментом на котором стоит ООП. Эту статью я написал по следам одного спора, как раз по поводу интерпретации термина Инкапсуляция. В ней я привожу разные точки зрения на него и пытаюсь докопаться до смысла заложенного в него при рождении.

https://boyarincev.net/articles/dotnet/what-is-encapsulation/

#misconceptions #oop
Если написав очередной switch/case, вы останавливались и задумывались - а нужно ли вообще обрабатывать default-ветку, а если обрабатывать, то бросать ли исключение, а если бросать то какое, а нормально ли вообще, что у меня эти switch/case по всей кодовой базе дублируются ? - то на эти вопросы я попробовал сформулировать своё мнение в этом тексте:
Какое исключение бросать в swith, если case не нашлось?
На работе у нас есть некое подобие самописной ORM, работающей напрямую с ADO.NET и одна из проблем с которой мы сталкивались при её разработке - это то, что у количества параметров используемых в DbCommand есть лимит, этот лимит накладывается базой данных и у разных баз данных он разный.

Например, у PostgreSQL в каждом SQL statement (под SQL statement имеется в виду то, что в разговорной речи называют SQL запросом) может использоваться не больше 65535 параметров (в одну DbCommand можно отправить множество SQL statement и таким образом в общем DbCommand может содержать больше 65535 параметров).

В большинстве запросов довольно сложно преодолеть разрешённую планку, мы столкнулись с этим ограничением в двух случаях - в Insert, когда за один запрос вставляется множество строк и в запросах с использованием оператора IN.

Первым делом я конечно же полез смотреть как эту проблему решает и решает ли вообще Entity Framework...

Написал небольшую заметку о том, можно ли при использовании Entity Framework столкнуться с ограничениями, которые накладывает использование DbCommand под его капотом: Entity Framework и ограничения DbCommand
Не используйте JObject.Parse() из JSON.NET

Для начала тест:

var json = "{ \"Date\":\"2024-09-14T00:00:00+04:00\"}";
var jsonObject = JObject.Parse(json);

Console.WriteLine(jsonObject.ToString());


Вопрос: Какая дата выведется в консоль ?

Ответ: Дата будет зависеть от вашей локальной таймзоны, от даты которую передают в json и от вашей локальной таблицы переводов времени в ОС, но если ваша локальная таймзона UTC, то скорее всего выведется

{
"Date": "2024-09-13T20:00:00+00:00"
}


Проблема в том, что JObject.Parse() умеет работать только с DateTime, поэтому во время его работы теряется часовой пояс переданного времени.

Вот несколько обсуждений этой проблемы:
https://stackoverflow.com/questions/46829888/how-parse-string-to-jobject-ignoring-time-zone
https://github.com/JamesNK/Newtonsoft.Json/issues/1110

В качестве решения предлагается использовать JsonConvert.DeserializeObject() - что далеко не всегда подходит, так как результат вызова Parse() позволяет манипулировать объектом как json'ом, например, исследовать его и изменять.

Интересно более глубокое раскрытие этой темы ?
Веб-разработка на .NET // dotnet web dev
Не используйте JObject.Parse() из JSON.NET Для начала тест: var json = "{ \"Date\":\"2024-09-14T00:00:00+04:00\"}"; var jsonObject = JObject.Parse(json); Console.WriteLine(jsonObject.ToString()); Вопрос: Какая дата выведется в консоль ? Ответ: Дата будет…
Вместо JObject.Parse(), кстати, можно использовать JOjbect.Load() - который Parse внутреннее и использует, но только в Load уже можно передать настроенный на парсинг DateTimeOffset JsonReader.
Вот текущая реализация Parse:

        public new static JObject Parse(string json, JsonLoadSettings? settings)
{
using (JsonReader reader = new JsonTextReader(new StringReader(json)))
{
JObject o = Load(reader, settings);

while (reader.Read())
{
// Any content encountered here other than a comment will throw in the reader.
}

return o;
}
}


А вот такой будет исправленная версия, поддерживающая парсинг в DateTimeOffset:

static JObject ParseWithDateTimeOffset(string json, JsonLoadSettings? settings)
{
using (JsonReader reader = new JsonTextReader(new StringReader(json)))
{
reader.DateParseHandling = DateParseHandling.DateTimeOffset;
JObject o = JObject.Load(reader, settings);

while (reader.Read())
{
// Any content encountered here other than a comment will throw in the reader.
}

return o;
}
}


И тогда результат выполнения кода:
var json = "{ \"Date\":\"2024-09-14T00:00:00+04:00\"}";
var jsonObject = ParseWithDateTimeOffset(json);

Console.WriteLine(jsonObject.ToString());


Будет:
{
"Date": "2024-09-14T00:00:00+04:00"
}
Библиотека, которая генерирует запрос для curl по HttpRequest - HttpClientToCurlGenerator

string curlScript = httpClient.GenerateCurlInString(httpRequestMessage);


Очень часто при проблемах с http-запросами нужно попробовать воспроизвести этот запрос вне приложения или передать его другой команде для исследования - что всегда геморрой, и в этом случае получить сразу готовый запрос для курла - бесценно.

Можно, например, прикрутить её вызов к обработке всех неуспешных статус кодов ответов и писать curl-запрос сразу в логи, единственный момент который в этом случае нужно учесть - это появление в логах логинов, паролей и прочих токенов, но это всё решаемо.
Сегодня начнём про приём дат во входящих запросах ASP.NET - в частности, про приём дат в json-теле запроса.

За привязку моделей из тела запроса у нас отвечают Input formatters.
Из коробки за привязку json отвечает SystemTextJsonInputFormatter

Он в свою очередь использует System.Text.Json, поэтому все особенности обработки дат, будут специфичны для этого сериализатора.

Есть статья описывающая как System.Text.Json работает с датами - DateTime and DateTimeOffset support in System.Text.Json, но она рассказывает далеко не о всех особенностях десериализации, поэтому будем подсматривать в исходном коде.

Начнём с десериализации дат из тела запроса в поля с типом DateTime в моделях.

Дата поддерживается в формате ISO 8601-1:2019

Нас интересует как десериализатор обработает вот такие даты:

"2024-02-08T00:00:00+00:00"
"2024-02-08T00:00:00+01:00"
"2024-02-08T00:00:00:10Z"
"2024-02-08T00:00:00"
"2024-02-08"

В исходном коде сначала выходим на TryGetDateTime в Utf8JsonReader а затем на JsonHelper.TryParseAsISO()

Правила следующие:

- Если есть offset, то дата создаётся c Kind Local и приводится к локальной дате согласно текущему часовому поясу системы.
- Если Z - то дата создаётся с Kind UTC
- В остальных случаях Kind Unspecified и время из присланной даты или 00:00:00, если времени не было

В первом случае с offset возможна потеря дня из-за часовых поясов.

    var jsonWithOffset0 = "{ \"Date\":\"2024-02-08T00:00:00+00:00\"}";
var jsonWithOffset1 = "{ \"Date\":\"2024-02-08T00:00:00+01:00\"}";
var jsonZ = "{ \"Date\":\"2024-02-08T00:00:00Z\"}";
var jsonDateTime = "{ \"Date\":\"2024-02-08T00:00:00\"}";
var jsonOnlyDate = "{ \"Date\":\"2024-02-08\"}";

Console.WriteLine($"Local TimeZone: {TimeZoneInfo.Local}");

var resultJsonWithOffset0 = System.Text.Json.JsonSerializer.Deserialize<DateTimeModel>(jsonWithOffset0);
Console.WriteLine(GetDateTimeString(resultJsonWithOffset0.Date));

var resultJsonWithOffset1 = System.Text.Json.JsonSerializer.Deserialize<DateTimeModel>(jsonWithOffset1);
Console.WriteLine(GetDateTimeString(resultJsonWithOffset1.Date));

var resultJsonZ = System.Text.Json.JsonSerializer.Deserialize<DateTimeModel>(jsonZ);
Console.WriteLine(GetDateTimeString(resultJsonZ.Date));

var resultJsonDateTime = System.Text.Json.JsonSerializer.Deserialize<DateTimeModel>(jsonDateTime);
Console.WriteLine(GetDateTimeString(resultJsonDateTime.Date));

var resultJsonOnlyDate = System.Text.Json.JsonSerializer.Deserialize<DateTimeModel>(jsonOnlyDate);
Console.WriteLine(GetDateTimeString(resultJsonOnlyDate.Date));

static string GetDateTimeString(DateTime dateTime, string json) => $"{dateTime} {dateTime.Kind} ({json})";

class DateTimeModel
{
public DateTime Date { get; set; }
}


Вывод:

Local TimeZone: (UTC) Время в формате UTC
08.02.2024 0:00:00 Local ({ "Date":"2024-02-08T00:00:00+00:00"})
07.02.2024 23:00:00 Local ({ "Date":"2024-02-08T00:00:00+01:00"})
08.02.2024 0:00:00 Utc ({ "Date":"2024-02-08T00:00:00Z"})
08.02.2024 0:00:00 Unspecified ({ "Date":"2024-02-08T00:00:00"})
08.02.2024 0:00:00 Unspecified ({ "Date":"2024-02-08"})


Во втором примере - 2024-02-08T00:00:00+01:00 при локальном часовом поясе +0 (время в UTC) мы во входящем запросе будем получать дату 07.02.2024 23:00:00

@boyarincevdotnet
Please open Telegram to view this post
VIEW IN TELEGRAM
Продолжим про десериализацию во входящих запросах ASP.NET - в частности, про приём дат в json-теле запроса.

Первая часть (https://teleg.eu/boyarincevdotnet/41) где мы посмотрели как десериализуется DateTime.

Сегодня посмотрим как десериализатор отработает по нашему тестовому набору дат:

"2024-02-08T00:00:00+00:00"
"2024-02-08T00:00:00+01:00"
"2024-02-08T00:00:00:10Z"
"2024-02-08"


Если целевой тип будет DateTimeOffse:

using System.Text.Json;
using System.Runtime.InteropServices;

var jsonWithOffset0 = "{ \"Date\":\"2024-02-08T00:00:00+00:00\"}";
var jsonWithOffset3 = "{ \"Date\":\"2024-02-08T00:00:00+03:00\"}";
var jsonZ = "{ \"Date\":\"2024-02-08T00:00:00Z\"}";
var jsonDateTime = "{ \"Date\":\"2024-02-08T00:00:00\"}";
var jsonOnlyDate = "{ \"Date\":\"2024-02-08\"}";

Console.WriteLine($"{RuntimeInformation.OSDescription}");

Console.WriteLine($"Local TimeZone: {TimeZoneInfo.Local}");

var resultJsonWithOffset0 = JsonSerializer.Deserialize<DateTimeOffsetModel>(jsonWithOffset0);
Console.WriteLine(GetDateTimeOffsetString(resultJsonWithOffset0.Date, jsonWithOffset0));

var resultJsonWithOffset3 = JsonSerializer.Deserialize<DateTimeOffsetModel>(jsonWithOffset3);
Console.WriteLine(GetDateTimeOffsetString(resultJsonWithOffset3.Date, jsonWithOffset3));

var resultJsonZ = JsonSerializer.Deserialize<DateTimeOffsetModel>(jsonZ);
Console.WriteLine(GetDateTimeOffsetString(resultJsonZ.Date, jsonZ));

var resultJsonDateTime = JsonSerializer.Deserialize<DateTimeOffsetModel>(jsonDateTime);
Console.WriteLine(GetDateTimeOffsetString(resultJsonDateTime.Date, jsonDateTime));

var resultJsonOnlyDate = JsonSerializer.Deserialize<DateTimeOffsetModel>(jsonOnlyDate);
Console.WriteLine(GetDateTimeOffsetString(resultJsonOnlyDate.Date, jsonOnlyDate));

string GetDateTimeOffsetString(DateTimeOffset dateTime, string json) =>
$"{dateTime} {dateTime.DateTime.Kind} ({json})";

class DateTimeOffsetModel
{
public DateTimeOffset Date { get; set; }
}


Результат:

Microsoft Windows 10.0.22621
Local TimeZone: (UTC) Время в формате UTC
08.02.2024 0:00:00 +00:00 Unspecified ({ "Date":"2024-02-08T00:00:00+00:00"})
08.02.2024 0:00:00 +01:00 Unspecified ({ "Date":"2024-02-08T00:00:00+01:00"})
08.02.2024 0:00:00 +00:00 Unspecified ({ "Date":"2024-02-08T00:00:00Z"})
08.02.2024 0:00:00 +00:00 Unspecified ({ "Date":"2024-02-08T00:00:00"})
08.02.2024 0:00:00 +00:00 Unspecified ({ "Date":"2024-02-08"})


Тут без сюрпризов - как мне кажется, мы получаем всё ровно так как и ожидаем.

Реализация в коде System.Text.Json
Please open Telegram to view this post
VIEW IN TELEGRAM
Десериализация DateOnly в json-теле запроса.

Поддержку DateOnly и TimeOnly добавили в System.Text.Json в .NET 7 (https://github.com/dotnet/runtime/issues/53539)

Реализацию можно посмотреть в DateOnlyConverter

Так как поддерживается только дата в формате yyyy-MM-dd, то проверять мы только её и будем:
    var jsonOnlyDate = "{ \"Date\":\"2024-02-08\"}";

Console.WriteLine($"{RuntimeInformation.OSDescription}");
Console.WriteLine($"Local TimeZone: {TimeZoneInfo.Local}");

var resultJsonOnlyDate = JsonSerializer.Deserialize<DateOnlyModel>(jsonOnlyDate);
Console.WriteLine(GetDateOnlyString(resultJsonOnlyDate.Date, jsonOnlyDate));

string GetDateOnlyString(DateOnly date, string json) => $"{date} ({json})";

public class DateOnlyModel
{
public DateOnly Date { get; set; }
}

И результат выполнения:
Microsoft Windows 10.0.22621
Local TimeZone: (UTC+03:00) Москва, Санкт-Петербург
08.02.2024 ({ "Date":"2024-02-08"})


Тут особо ничего не прокомментируешь - максимально прозрачное и ожидаемое поведение, единственной проблемой может быть необходимость использования .NET 7.
The Benefits of Using a YT Audio to MP3 Converter