.NET Разработчик
6.51K subscribers
427 photos
2 videos
14 files
2.04K links
Дневник сертифицированного .NET разработчика.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День 1402.
35 Заблуждений о Дате и Времени. Окончание
Начало
Продолжение

24. Переход на летнее время происходит одновременно во всех часовых поясах
Летнее время в Чили начинается в полночь, а в зоне Америка/Торонто - в 2 часа ночи. В Европе переход на летнее время происходит в последнее воскресенье марта, а в Северной Америке - во второе воскресенье марта.

25. Недели начинаются в понедельник
В Израиле неделя начинается в воскресенье.

26. Выходные - это суббота и воскресенье
Многие страны используют разные правила для рабочей недели и выходных. Вот несколько примеров:
Канада: суббота-воскресенье
Израиль: пятница-суббота
Иран: пятница
Непал: суббота

27. Месяц длится 28-31 день
Википедия говорит, что месяцы могут длиться 32 дня в юлианском календаре. Также см. пример из п.4.

28. Год начинается 1 января
В Великобритании до 1752 года юридический год начинается 25 марта. Так, например, за 24 марта 1707 г. сразу же следовало 25 марта 1708 г., а днем, следующим за 31 декабря 1708 г., был 1 января 1708 г.
В эфиопском календаре год начинается 11 или 12 сентября.
Другие примеры есть в статье Википедии про Новый год.

29. Существует нулевой год
В системе Anno Domini (AD), которая используется для нумерации лет по григорианскому календарю, за годом 1 до н.э. непосредственно следует год 1 н.э. Таким образом, года 0 не существует.

30. Об изменениях в часовых поясах предупреждают заранее
Аргентина уведомила за 11 дней о своем решении отменить переход на летнее время в 2009/2010 гг.
В 2011 Турция уведомила за две недели о том, что откладывает переход на летнее время.
В 2013 Марокко менее чем за день уведомили об изменении своих правил часового пояса.
В 2022 Чили уведомили за 1 месяц, что меняют правила часового пояса. Для компаний, использующих ежемесячные обновления, это может быть сложно реализовать. В Майкрософт даже выпустили особое руководство на этот случай.
Ну и не стоит забывать про метания России то в летнее, то в зимнее время.

31. DateTime.UtcNow равномерно увеличивается
Вы можете изменить системное время вручную или оно может обновиться с NTP-сервера. Поэтому DateTime.UtcNow <= DateTime.UtcNow может выдать false, если между вызовами произошла корректировка назад.

32. При отображении даты вы получите тот же год, что и в объекте DateTime
Некоторые библиотеки позволяют различать год (например, yyyy) и год как неделю ISO (например, YYYY). В этом случае отображаемый год может отличаться от года, хранящегося в объекте DateTime. Например, 2022-01-02 — это 52-я неделя 2021 года.

33. 32 бита достаточно, чтобы хранить любую дату
При использовании 32-битных целых чисел со знаком временная метка Unix позволяет представлять дату до 2038 года. Чтобы избежать такого ограничения, используйте 64-битные целые числа.
В некоторых тестах у .NET возникали проблемы с производительностью примерно каждые 29 дней https://github.com/dotnet/runtime/issues/51935. Основная причина заключалась в том, что реализация пула потоков использовала Environment.TickCount, 32-разрядное значение со знаком.

34. Хранение даты в формате UTC предотвращает все проблемы
Если вы хотите запланировать встречу в следующем апреле в 10 утра в Нью-Йорке, не стоит вычислять дату в формате UTC и хранить её. Ведь нельзя быть уверенным, что правила перехода на летнее время не изменятся до встречи.

35. Синхронизация времени между машинами проста
Протокол Network Time Protocol (NTP) позволяет синхронизировать время между машинами. Есть несколько факторов, которые могут затруднить получение точного времени:
- Сервер NTP может быть неточным
- Сеть может быть перегружена
- Сеть может быть несимметричной (не то же количество переходов для запроса к серверу, что и количество переходов для ответа).
- Машинные часы могут быть неточными, поэтому машинные часы дрейфуют довольно часто.
Подробнее об этом было в недавнем посте.

Источник: https://www.meziantou.net/misconceptions-about-date-and-time.htm
День 1403. #юмор
👍13
День 1404. #ЧтоНовенького #DotNet8
Новые Анализаторы в .NET 8
.NET 7 вышел, пора посмотреть, что нас ждёт в новой версии))) Сегодня рассмотрим новые анализаторы кода. Анализаторы в версии превью можно использовать, если добавить в файл .csproj следующее:
<PropertyGroup Label="Analyzer settings">
<AnalysisLevel>preview</AnalysisLevel>
</PropertyGroup>

Уже сейчас есть более 100 правил, которые предупредят вас в определённых ситуациях. Полный список можно найти на странице GitHub. Сегодня рассмотрим, что нового собираются добавить.

Строки
1. Рекомендация заменить string.ToLower() на регистронезависимый Equals
По сути, правило предложит заменить
str.ToLowerCase() == "string";
на
string.Equals(str, "string", StringComparison.OrdinalIgnoreCase);
Это быстрее и не аллоцирует строки.

2. Рекомендация использовать StartsWith вместо IndexOf == 0
Проблема с IndexOf в том, что в худшем случае он пройдёт по всей строке. StartsWith, напротив, сразу прервётся при первом несоответствии, поэтому он быстрее.

Коллекции
3. Рекомендация использовать Length/Count/IsEmpty вместо Any()
Если в коллекции есть свойство вроде Length (T[]), Count (List<T>) или IsEmpty (потокобезопасные коллекции), то лучше использовать его, а не Any().
// Это будет помечено новым анализатором
var isEmpty = names.Any();
// Это не будет помечено как "предпочтительное"
var isEmpty = names.Count != 0;
Это может быть быстрее, т.к. Any выполняет проверку типов, а Length или Count — это простые свойства, которые, помимо прочего, могут быть встроены. Хотя, субъективно, в коде Any может выглядеть более читабельно.

4. Предотвращение неправильного использования Split
Допустим, вы хотите разделить строку по пробелам и табам:
var split = s.Split(' ', '\t');
Это прекрасно работает. Но если вы захотите удалить пустые элементы:
var split = s.Split(' ', '\t',
StringSplitOptions.RemoveEmptyEntries);
Но это не будет работать правильно, потому что второй char преобразуется в int и будет использован в качестве аргумента count (максимального количества элементов в результирующем массиве) для перегруженной версии Split. Правильное использование будет таким:
var split = s.Split(new[] { ' ', '\t' },
StringSplitOptions.RemoveEmptyEntries);

Структуры
6. Атрибут NonCopyable
Обычно, если вы передаёте типы-значения в функцию, они копируются. В чём проблема? Предположим, вы пишете какой-то высокопроизводительный API (например, ValueStringBuilder), используя (ref) struct, чтобы избежать аллокаций на куче. Потребители этих типов должны быть осторожны, чтобы объект не копировался повсюду. И вот тут-то поможет новый атрибут.
[NonCopyable]
public ref struct ValueStringBuilder
{ … }

// Использование
public void MyMethod(ValueStringBuilder vsb)
{ … }

var vsb = new ValueStringBuilder();
MyMethod(vsb);

Здесь будет ошибка, т.к. ValueStringBuilder должен передаваться по ссылке. Правильное использование:
public void MyMethod(ref ValueStringBuilder vsb)
{ … }

var vsb = new ValueStringBuilder();
MyMethod(ref vsb);

Источник: https://steven-giesel.com/blogPost/1a03a998-e428-4a42-9c30-1cfbf6ac5f4c
👍20
День 1405. #МоиИнструменты
Интеграционные Тесты
ASP.NET Core с Помощью Alba
Alba — это небольшая библиотека, которая обеспечивает простое интеграционное тестирование маршрутов ASP.NET Core, полностью совместимая с NUnit/xUnit.Net/MSTest. Недавно вышедшая версия 7.1 поддерживает .NET 7, улучшила обработку JSON для конечных точек Minimal API и поддерживает составной тип содержимого форм.

Допустим, есть проект Minimal API:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello, World!");
app.Run();

Мы можем написать тест маршрута, вроде такого:
[Fact]
public async Task sample_test()
{
// Здесь для примера хост создаётся внутри теста.
// В реальности это дорого,
// его лучше создать один на все тесты.
await using var host =
await AlbaHost.For<global::Program>();

var result = await host.GetAsText("/");

Assert.Equal(result, "Hello, World!");
}

Тест загружает ваше фактическое приложение, используя его конфигурацию, но используя TestServer вместо Kestrel в качестве веб-сервера.
Помимо GetAsText() есть другие полезные методы, вроде PostJson(), который использует конфигурацию сериализации JSON вашего приложения, если вдруг она у вас кастомизирована. Аналогично Receive<T>() использует сериализацию JSON вашего приложения.
Когда тест выполняется, он проходит через весь конвейер ASP.NET Core вашего приложения, включая любое зарегистрированное промежуточное ПО.

Альтернативным вариантом для теста может быть использование класса Scenario и его удобочитаемых методов:
await host.Scenario(_ =>
{
_.Get.Url("/");
_.ContentShouldBe("Hello, World!");
_.StatusCodeShouldBeOk();
});

Библиотека предлагает множество настроек и вариантов использования, которые можно посмотреть в документации.

Имейте в виду, чтобы ваши тесты «видели» ваш класс Program, вам нужно разрешить вашему тестовому проекту «видеть» ваш основной проект. Для этого в файл .csproj основного проекта добавьте:
<ItemGroup>
<InternalsVisibleTo Include="[ИмяПроектаТестов]" />
</ItemGroup>

Источник: https://jeremydmiller.com/2022/11/28/alba-for-effective-asp-net-core-integration-testing/
👍21
День 1406. #ЗаметкиНаПолях #AsyncTips
Планирование работы в пуле потоков

Любой блок кода должен выполняться в каком-то потоке. Планировщик (scheduler) решает, где должен выполняться тот или иной код. Обычно планировщик по умолчанию работает как надо. Например, оператор await в асинхронном коде автоматически возобновит выполнение метода в том же контексте, если только вы не переопределите это поведение.

Задача: имеется фрагмент кода, который должен выполняться в потоке из пула потоков.

Решение
В большинстве случаев следует использовать Task.Run. Следующий пример блокирует поток из пула на 2 секунды:
Task task = Task.Run(() =>
{
Thread.Sleep(2000);
});

Task.Run также поддерживает возвращаемые значения и асинхронные лямбда-выражения. Задача, возвращаемая Task.Run в следующем коде, завершится через 2 секунды с результатом 13:
Task<int> task = Task.Run(async () =>
{
await Task.Delay(2000);
return 13;
});

Task.Run возвращает объект Task (или Task<T>), который может естественным образом потребляться асинхронным или реактивным кодом.

Task.Run идеально подходит для UI-приложений с продолжительной работой, которая не должна выполняться в UI-потоке. Тем не менее не используйте Task.Run в ASP.NET, если только вы не уверены в том, что делаете. В ASP.NET код обработки запросов уже выполняется в потоке из пула, так что перенесение его в другой поток из пула обычно нерационально.

Task.Run является фактической заменой для BackgroundWorker, Delegate.BeginInvoke и ThreadPool.QueueUserWorkItem. Ни один из этих старых API не следует использовать в новом коде; код с Task.Run намного проще писать и сопровождать.

Task.Run справляется с большинством задач, для которых используется Thread, так что в большинстве случаев Thread может заменяться на Task.Run (за редким исключением).

Параллельный код и код потоков данных выполняется в пуле потоков по умолчанию, поэтому обычно Task.Run не нужно использовать с кодом, выполняемым через Parallel, библиотекой TPL Dataflow или Parallel LINQ.
Если вы применяете динамический параллелизм, используйте Task.Factory.StartNew вместо Task.Run. Это необходимо из-за того, что у объекта Task, возвращаемого Task.Run, параметры по умолчанию настроены для асинхронного использования (т.е. для потребления в асинхронном или реактивном коде). Кроме того, он не поддерживает такие расширенные возможности, как задачи «родитель/потомок», типичные для динамического параллельного кода.

Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 13.
👍19
День 1407. #ЗаметкиНаПолях #TipsAndTricks
Современные Приёмы в C#. Часть 3. Начало
Часть 1
Часть 2

Компиляция Обобщённого Кода

Шаблоны C++
Шаблоны C++ (и дженерики) являются формами полиморфных функций. Вы определяете один тип (или метод), который принимает параметр типа и может изменить своё поведение на основе переданного типа. Это просто конструкция времени компиляции. Они указывают компилятору, как генерировать код для типа/метода шаблона. А компилятор выполняет преобразование, называемое мономорфизацией: для каждого аргумента шаблона, фактически переданного типу/методу, компилятор создаёт новую копию типа/метода.

Обобщения в С#
В С# это конструкция времени выполнения: компилятор фактически выводит сам обобщённый тип/метод в IL (промежуточный язык). Во время выполнения реализация обобщённого типа/метода распределяется между обобщёнными аргументами.

Другими словами, обобщения в C# не подвергаются мономорфизации для ссылочных типов. Существует только одна копия реализации типа/метода, которая является общей для всех. Однако для типов-значений обобщения подвергаются мономорфизации!

Это имеет смысл: если метод Something<T> определяет значение локальной переменной T, компилятору необходимо знать, насколько велико это значение. Размер ссылки одинаков независимо от типа, на который она ссылается, но размер типа-значения может различаться.

Обобщения и ограниченные типы значений
Мономорфизация наиболее полезна, если ограничиваете обобщения определённым интерфейсом. Простой и бесполезный пример:
interface ISample
{
int Setting { get; }
}

void Function<T>()
where T : struct, ISample
{
if (default(T).Setting == 13)
Console.WriteLine("Тринадцать!");
else
Console.WriteLine(default(T).Setting);
}

readonly struct Sample7 : ISample
{
public int Setting => 7;
}

readonly struct Sample13 : ISample
{
public int Setting => 13;
}

Function<Sample7>(); // 7
Function<Sample13>();// Тринадцать!

Компилятор C# обрабатывает Function как обычную обобщённую функцию. JIT создаст две отдельные копии Function, поскольку Sample7 и Sample13 являются типами-значений. В обеих копиях код default(T).Setting передаётся как ограниченный виртуальный вызов.

Тогда каждая копия метода имеет высокую вероятность оптимизации. Компилятор знает тип T для каждой копии. Когда он оптимизирует Function<Sample7>, он знает, что default(T).Setting вызывает метод ISample.get_Setting для типа Sample7. Реализация ISample.Setting в Sample7 тривиальна и, скорее всего, будет встроена, что означает, что ветвь if может быть предварительно вычислена. Очень вероятно, что обе копии Function<T> в итоге будут иметь только один вызов Console.WriteLine без оператора if.

Статические абстрактные методы интерфейса позволяют нам ещё немного почистить код. Вместо того, чтобы определять Setting как метод экземпляра, он может быть статическим абстрактным методом:
interface ISample
{
static abstract int Setting { get; }
}

void Function<T>()
where T : struct, ISample
{
if (T.Setting == 13)
Console.WriteLine("Тринадцать!");
else
Console.WriteLine(T.Setting);
}

Теперь свойства Sample7 и Sample13 будут статическими, зато нет необходимости в default(T).

Окончание следует…

Источник:
https://blog.stephencleary.com/2022/10/modern-csharp-techniques-3-generic-code-generation.html
👍12
День 1408. #ЗаметкиНаПолях #TipsAndTricks
Современные Приёмы в C#. Часть 3. Окончание
Часть 1
Часть 2
Часть 3. Начало

Пример применения
В кодовой базе бывают области, где определённые аргументы метода всегда являются константами. Обычно это указывает на то, что метод следует разделить на два, но это не всегда возможно. Возьмём популярный костыль с булевым аргументом, отвечающим за синхронное или асинхронное исполнение метода:
private async Task<string> GetCoreAsync(bool sync)
{
if (sync)
Thread.Sleep(TimeSpan.FromSeconds(1));
else
await Task.Delay(TimeSpan.FromSeconds(1));
return "Hi!";
}

public string Get() =>
GetCoreAsync(true).GetAwaiter().GetResult();

public Task<string> GetAsync() =>
GetCoreAsync(false);

Допустим, что GetCoreAsync намного длиннее и сложнее, и создание двух разных методов действительно усложнит обслуживание.

Воспользуемся обобщённой генерацией кода для создания двух разных методов:
Извлечём различия в коде (Thread.Sleep и Task.Delay). Для них потребуется определение в интерфейсе, и они будут реализованы для каждого типа-значения. Поскольку код может быть синхронным или асинхронным, мы будем использовать ValueTask в качестве типа результата. GetCoreAsync может просто вызывать эти методы интерфейса:
private interface IDelay
{
static abstract ValueTask
DelayAsync(TimeSpan delay);
}

private readonly struct SyncDelay : IDelay
{
public static ValueTask
DelayAsync(TimeSpan delay)
{
Thread.Sleep(delay);
return new();
}
}

private readonly struct AsyncDelay : IDelay
{
public static async ValueTask
DelayAsync(TimeSpan delay) =>
await Task.Delay(delay);
}

private async Task<string> GetCoreAsync<TDelay>()
where TDelay: struct, IDelay
{
await TDelay.DelayAsync(TimeSpan.FromSeconds(1));
return "Hi!";
}

public string Get() =>
GetCoreAsync<SyncDelay>()
.GetAwaiter().GetResult();

public Task<string> GetAsync() =>
GetCoreAsync<AsyncDelay>();

Основная реализация (GetCoreAsync) упрощена и более очевидна. Публичный интерфейс (Get и GetAsync) вообще не изменился. И во время выполнения, если используется только один путь, то только один путь будет скомпилирован JIT-компилятором. Если используются оба пути, JIT создаст две копии GetCoreAsync, каждая из которых оптимизирована для своей ситуации (асинхронной или синхронной). Это особенно полезный метод для библиотек, которым может потребоваться предоставлять обе формы методов, но существует высокая вероятность использования только одного из них.

Стивен Тауб рассказывает в недавнем посте, как BCL использует эту технику.

Примечания и Ограничения
Используя дженерики C# с типами-значений, мы можем обеспечить мономорфизацию, однако остальная часть поведения не гарантируется. JIT не гарантирует, что какие-либо конкретные методы будут встроены или что произойдет какая-либо оптимизация. Разумно предположить, что она будет иметь место, а при современной многоуровневой оптимизации можно также ожидать, что метод станет более оптимизированным, если вызывается много раз.

Вы ограничены только тем, что может быть выражено в виде членов интерфейса. Вы можете определять методы и свойства, но не константные значения или вложенные типы.

Источник: https://blog.stephencleary.com/2022/10/modern-csharp-techniques-3-generic-code-generation.html
👍3
День 1409. #Тестирование
Динамическое Тестирование Безопасности Приложений
Dynamic application security testing (DAST) может помочь обнаружить недостатки безопасности в вашем коде. И может делать это автоматически в процессе сборки.

В прошлом автоматизированное тестирование безопасности было довольно медленным, ресурсоемким, и часто результаты были полны ложных срабатываний. Сегодня существует DAST — это ПО, которое взаимодействует с запущенным приложением, автоматически отправляя запросы и ответы, пытаясь найти уязвимости в веб-приложениях и API. Если приложение или API находится в Интернете, это означает, что любой может сканировать его с помощью динамического сканера. Хотя существуют системы, которые могут блокировать некоторые из атак, большинство сайтов не используют их (из-за стоимости, задержки и требуемых ресурсов), т.е. большинство веб-приложений и API хотя бы один раз были просканированы на уязвимости.

DAST будет взаимодействовать с приложением сначала пассивно (чтение запросов и ответов), а затем активно (создание или обработка запросов, а затем корректировка на основе ответов в попытках найти уязвимости).

Большинство разработчиков могут настроить DAST в течение часа, а ещё через час найти хотя бы одну уязвимость. Вы не найдете 100% уязвимостей, как при профессиональном пен-тесте, но наверняка найдете много, включая все очевидные. Это самые важные уязвимости, которые нужно исправить в первую очередь, поскольку их может обнаружить и неопытный злоумышленник.

Чтобы DAST мог протестировать ваше приложение, оно должно быть запущено; доступ к коду не требуется. Приложение может работать на виртуальной машине, в контейнере или как служба. Некоторые динамические сканеры позволяют выполнять ручное тестирование, т.е. вы можете вручную создавать запросы, в то время как другие выполняют фаззинг, который проверяет валидацию ввода приложения.

DAST может работать по принципу «plug-n-play», подразумевая, что настройка практически не требуется. Но тогда придётся подождать несколько часов для получения результатов и запускать каждое сканирование вручную.

Один из вариантов ускорить тесты - создать архивные файлы HTML (HAR) и передать их в DAST. Если у вас есть команда обеспечения качества (QA), которая создаёт файлы HAR для автоматизации тестирования UI, эта стратегия может работать очень хорошо. DAST проверяет только те функции и код, которые являются частью HAR. Тесты пройдут значительно быстрее, и будет тестироваться только небольшая часть приложения, которая содержится в HAR. Однако, если вы не создаёте и не обновляете файлы HAR, добавится много работы.

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

Ещё вариант — удалить любые тесты, которые просто не применимы к тому, что вы создали. Если вы создали приложение .Net Core, размещённое в Azure с базой данных MS SQL, вам не нужно запускать тесты для MongoDB, AWS, WordPress и множество других, включённых в большинство DAST.

Однако для достижения наилучших результатов следует регулярно сканировать всё приложение. Можно запускать тесты на ночь или в нерабочее время. Если вы хотите запустить динамическое сканирование в рабочей среде, следует убедиться, что вы отключили функцию фаззинга, так как вы можете случайно испортить живые данные. Выполните фаззинг в зеркале продакшена и убедитесь, что вы сначала сделали резервную копию данных — на всякий случай.

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

Источник: https://stackoverflow.blog/2022/11/30/continuous-delivery-meet-continuous-security/
👍6
День 1410. #ЧтоНовенького #DotNet8
Замороженные Коллекции
Продолжаем рассматривать, чего новенького могут предложить нам в 8й версии .NET. Сегодня рассмотрим, что такое замороженные коллекции и как они работают.

Вот код, который работает в альфа-версии .NET8:
List<int> list = new() { 1, 2, 3 };
ReadOnlyCollection<int> readonlyList =
list.AsReadOnly();
ImmutableList<int> immutableList =
list.ToImmutableList();
FrozenSet<int> frozenSet =
list.ToFrozenSet();

list.Add(4);

list.Count; // 4
readonlyList.Count; // 4
immutableList.Count; // 3
frozenSet.Count; // 3

ReadOnlyList — это просто «представление» объекта, из которого он был создан. Поэтому, если вы обновите список, из которого он был первоначально создан, ReadOnlyList также отразит это изменение. Пользователь ReadOnlyList просто не имеет возможности изменить внутреннее состояние самого списка.

Замораживаемую коллекцию можно изменять до тех пор, пока она не будет заморожена. После этого она больше не отражает изменений. Поэтому Count = 3.
Неизменяемый список также содержит только 3 элемента, но при этом неизменяемые коллекции имеют эффективные способы изменения списка путем создания нового:
// это создаст новый неизменяемый список
var newList = immutableList.Add(2);
Для замороженных коллекций такого поведения не предусмотрено.

На данный момент в .NET8 есть два типа замороженных коллекций: FrozenSet и FrozenDictionary. Возникает вопрос: зачем они?

Замороженные объекты вследствие своих ограничений могут дать преимущества в производительности. Часто коллекции инициализируются в какой-то момент, но никогда не меняют своего состояния. Например, посмотрим для списка из чисел от 1 до 1000 тест скорости метода Contains:
public void Lookup()
{
for (var i = 0; i < 1000; i++)
_ = list.Contains(i);
}

List: 57.561 us
FrozenSet: 1.963 us
HashSet: 2.997 us
ImmutableHashSet: 15.422 us

Особенности замороженных коллекций:
1. Они действительно доступны только для чтения и неизменны. Они создаются таким образом, чтобы данные были плотными и оптимально организованными для быстрого чтения.
2. При создании находится оптимальный размер таблицы сегментов, чтобы уменьшить или устранить коллизии хэшей, что сокращает среднее время поиска.
3. Особое внимание уделяется коллекциям со строковыми ключами. Различные компараторы выбираются динамически для повышения скорости хеширования во время поиска. Код просматривает все ключи и пытается найти кратчайшую подстроку, которая создаёт уникальный набор ключей. Обычно это значительно сокращает количество символов, используемых при вычислении хэш-кодов во время поиска.
4. Эти коллекции могут содержать не более 64КБ состояния, что покрывает 99% случаев.

Очевидно, что создание замороженной коллекции обходится дороже, чем создание простого списка. Это отражает типичный вариант их использования. Создать один раз в начале, а затем часто обращаться к ним (например, через поиск элемента).

Вот репозиторий с примерами кода и тестами.

Источник: https://steven-giesel.com/blogPost/34e0fd95-0b3f-40f2-ba2a-36d1d4eb5601
👍17
Developing-on-AWS-with-CSharp.pdf
12.7 MB
День 1411. #Книги
Сегодня порекомендую вам (и сохраню для себя, т.к. ещё не читал) книгу «Developing on AWS with C#», O’Reilly Media, 2023.

Авторы Ноа Гифт, основатель Pragmatic AI Labs, и Джеймс Чарльзуорт, технический руководитель Pendo, расскажут вам о широте инструментов .NET на AWS. Вы изучите методы использования контейнеров Linux и Windows и бессерверной архитектуры для создания, обслуживания и масштабирования современных приложений .NET на AWS. Из этой книги вы узнаете, как сделать ваши приложения более современными, отказоустойчивыми и экономичными. Кроме того, в книге описано:
- Как начать создавать решения с помощью C# на AWS.
- Какие существуют лучшие практики DevOps для AWS, инструменты и сервисы разработки, которые предоставляет AWS.
- Как успешно перенести устаревшее приложение .NET на AWS.
- Как разрабатывать бессерверные микросервисы .NET на AWS.
- Как использовать контейнеризацию, переместить приложение в облако, отслеживать и тестировать.
👍15
День 1412. #ЗаметкиНаПолях #AsyncTips
Выполнение Кода с Помощью Планировщика. Начало: Создание Планировщика

Задача
Есть несколько частей кода, которые требуется выполнить определённым способом. Например, все они должны выполняться в UI-потоке или же в любой момент времени должно выполняться только определённое количество частей.

Решение
Здесь рассмотрим тип TaskScheduler, хотя, в .NET есть и другие способы решения этой задачи.

Простейшая разновидность — TaskScheduler.Default — ставит работу в очередь пула потоков. Его редко придётся использовать явно, потому что он используется по умолчанию во многих сценариях планирования: в Task.Run, в параллельном коде и в коде потоков данных.

Вы можете сохранить конкретный контекст и позднее спланировать работу в этом контексте:
var scheduler = 
TaskScheduler
.FromCurrentSynchronizationContext();
Этот код создаёт объект TaskScheduler, чтобы сохранить текущий контекст и спланировать выполнение кода в нём. Тип SynchronizationContext представляет контекст планирования общего назначения. В .NET предусмотрено несколько разных контекстов: многие UI-фреймворки предоставляют контекст SynchronizationContext, представляющий UI-поток, а в ASP.NET до Core предоставлялся контекст SynchronizationContext, представляющий контекст запроса HTTP. Также возможно напрямую использовать SynchronizationContext для выполнения кода в этом контексте; но это не рекомендуется. Там, где это возможно, используйте await для возобновления в неявно сохранённом контексте либо TaskScheduler.

ConcurrentExclusiveSchedulerPair — тип, представляющий в действительности два планировщика, связанных друг с другом:
- ConcurrentScheduler позволяет нескольким задачам выполняться одновременно, при условии, что ни одна задача не выполняется в ExclusiveScheduler;
- ExclusiveScheduler выполняет только по одной задаче за раз и только если в настоящее время никакие задачи не выполняются в ConcurrentScheduler.
var schPair = new ConcurrentExclusiveSchedulerPair();
var concurrent = schPair.ConcurrentScheduler;
var exclusive = schPair.ExclusiveScheduler;

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

Также тип может применяться в качестве регулирующего планировщика, который будет ограничивать собственный уровень параллелизма. При этом ExclusiveScheduler обычно не используется:
var schPair = new ConcurrentExclusiveSchedulerPair(
TaskScheduler.Default,
maxConcurrencyLevel: 8);
var scheduler = schPair.ConcurrentScheduler;

Заметьте, что такая регулировка влияет на код только во время его выполнения. В частности, асинхронный код не считается выполняемым во время ожидания операции. ConcurrentScheduler регулирует выполняющийся код; тогда как другие виды регулировки (такие, как SemaphoreSlim) осуществляют регулировку на более высоком уровне (т.е. всего async-метода.)

Заметьте, что конструктору ConcurrentExclusiveSchedulerPair передаётся объект TaskScheduler.Default. Это объясняется тем, что ConcurrentExclusiveSchedulerPair применяет свою логику конкурентности/эксклюзивности выполнения к существующему TaskScheduler.

Никогда не используйте платформенно-зависимые типы для выполнения кода в UI-потоке. WPF, Silverlight, iOS и Android предоставляют тип Dispatcher, Universal Windows использует тип CoreDispatcher, а в Windows Forms существует интерфейс ISynchronizeInvoke (т. е. Control.Invoke). Не используйте эти типы в новом коде; просто считайте, что их вообще нет. Эти типы только без всякой необходимости привязывают код к конкретной платформе. SynchronizationContext — абстракция общего назначения на базе этих типов.

Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 13.
👍11
День 1413. #ЗаметкиНаПолях #AsyncTips
Выполнение Кода с Помощью Планировщика. Окончание: Использование Планировщика

Задача:
требуется управлять выполнением отдельных фрагментов в параллельном коде.

Решение
После того как вы создадите экземпляр TaskScheduler (см. Создание Планировщика), можете включить его в набор параметров, передаваемых методу Parallel. Следующий код получает набор коллекций матриц, запускает несколько параллельных циклов и ограничивает общий параллелизм всех циклов одновременно независимо от количества матриц в каждом наборе:
void RotateMatrices(
IEnumerable<IEnumerable<Matrix>> collections,
float degrees)
{
var schPair = new ConcurrentExclusiveSchedulerPair(
TaskScheduler.Default,
maxConcurrencyLevel: 8);
var scheduler = schPair.ConcurrentScheduler;
var opts = new ParallelOptions {
TaskScheduler = scheduler };

Parallel.ForEach(
collections,
opts,
matrices => Parallel.ForEach(
matrices,
opts,
m => m.Rotate(degrees)));
}

Parallel.Invoke также получает экземпляр ParallelOptions, поэтому вы можете передать TaskScheduler при вызове Parallel.Invoke так же, как и для Parallel.ForEach. При выполнении динамического параллельного кода можно передать TaskScheduler непосредственно TaskFactory.StartNew или Task.ContinueWith.

Передать TaskScheduler коду Parallel LINQ (PLINQ) невозможно.

Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 13.
👍3
День 1414. #ЧтоНовенького
Новые Возможности MVC в .NET 7. Часть 1
В этой серии рассмотрим некоторые нововведения в ASP.NET Core в .NET 7.

1. IParseable/TryParse для привязки примитивных типов
MVC использует подход «поставщика привязки». Можно привязать параметры методов действий к значениям формы, строки запроса, заголовкам и т.п. По умолчанию MVC связывает некоторые простые типы или любые типы, имеющие TypeConverter из строки. Всё остальное рассматривается как сложный тип, т.е. либо привязывается к телу запроса, либо привязывается не сам тип, а каждое из отдельных свойств типа. Допустим, у нас есть тип:
public class Range
{
public int From { get; set; }
public int To { get; set; }
}
и привязка его в методе действия контроллера:
public class MyController : ControllerBase
{
public IEnumerable<MyClass> Get(Range days)
{ … }
}
В .NET 6 вам нужно либо написать TypeConverter для Range, либо привязать свойства From и To:
/MyController?from=1&to=3

В .NET 7 есть другой вариант. Можно реализовать интерфейс IParseable<T> и создать объект Range из любой строки. Например, мы хотим иметь такой URL:
/MyController/1-3

Формально реализовывать IParseable<T> не нужно, достаточно реализовать метод TryParse(). Пример реализации ниже. Детали не особенно важны, и здесь не учитывается множество пограничных случаев:
public readonly struct Range : IParsable<Range>
{
public int From { get; }
public int To { get; }

public Range(int from, int to)
{
From = from;
To = to;
}

public static bool TryParse(
string? value,
IFormatProvider? provider,
out Range result)
{
if (value != null)
{
var sep = value.IndexOf('-');
if (sep > 0 && sep < value.Length - 1)
{
var fromSpan = value.AsSpan().Slice(0, sep);
var toSpan = value.AsSpan(sep + 1);

if (int.TryParse(fromSpan,
out var from)
&& int.TryParse(toSpan,
out var to))
{
result = new Range(from, to);
return true;
}
}
}

result = default;
return false;
}

public static Range Parse(
string value,
IFormatProvider? provider)
{
if (!TryParse(value, provider, out var result))
throw new ArgumentException();

return result;
}
}

Затем можно просто добавить атрибут к методу контроллера:
[HttpGet("{days}")]
public IEnumerable<MyClass> Get(Range days)
{ … }

Проще ли реализовать IParseable<T>, чем написать свой связыватель модели? Вероятно. Проще, чем свой TypeConverter? Возможно. Однако при этом подходе вы можете использовать одни и те же типы в минимальных API и в контроллерах. Это должно упростить переход от минимальных API к контроллерам (и наоборот).

Если же вы хотите отключить эту функцию, удалите TryParseModelBinderProvider из MvcOptions:
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MvcOptions>(opts => {
opts.ModelBinderProviders
.RemoveType<TryParseModelBinderProvider>();
});

Источник: https://andrewlock.net/5-new-mvc-features-in-dotnet-7/
👍8
День 1415. #ЧтоНовенького
Новые Возможности MVC в .NET 7. Часть 2
В этой серии рассмотрим некоторые нововведения в ASP.NET Core в .NET 7.
Часть 1

2. Контроллеры MVC могут автоматически определять сервисы
Контроллеры в .NET 7 автоматически определяют сервисы, зарегистрированные в контейнере внедрения зависимостей, не требуя явного атрибута [FromServices]. В минимальных API вы можете просто внедрять сервисы непосредственно в конечные точки API:
app.MapGet("/", (IGreeterService service)
=> service.SayHello();

Эквивалент в .NET 6 выглядел бы так:
public class SomeController : Controller
{
public void IActionResult Get(
[FromServices] IGreeterService service)
{
return service.SayHello();
}
}

В .NET 6, атрибут [FromServices] обязателен, иначе MVC попытается связять параметр service с телом запроса. В .NET 7 можно обойтись без него:
public class SomeController : Controller
{
public void IActionResult
Get(IGreeterService service)
{
return service.SayHello();
}
}

Эта функциональность основывается на сервисе IServiceProviderIsService, представленном в .NET 6, и должна «просто работать». Понятно, что [FromService] вряд ли будет использоваться с контроллерами, гораздо более предпочтительный вариант будет внедрение через конструктор, но тем не менее это потенциально удаляет немного шаблонного кода и снова идёт в ногу с минимальными API.

Кстати, о IServiceProviderIsService. Смысл в нём в том, что нужен способ узнать, что за параметр передаётся методу действия. Одним (плохим) решением было бы, чтобы API всегда предполагал, что это сервис, и пытался получить его из контейнера DI. Однако при этом будет предпринята попытка создать экземпляр сервиса, что может иметь непредвиденные последствия. Представьте, что контейнер записывает в журнал каждую попытку получить несуществующий сервис, чтобы вам было легче обнаруживать неверные настройки. Это может повлиять на производительность, если запись в журнал будет делаться для каждого отдельного запроса.

Итак, сервис IServiceProviderIsService:
public partial interface IServiceProviderIsService
{
bool IsService(Type serviceType);
}
Он предоставляет единственный метод, который можно вызвать, чтобы проверить, зарегистрирован ли данный тип сервиса в контейнере внедрения зависимостей. Сам сервис IServiceProviderIsService также можно получить из контейнера. Если вы используете пользовательский контейнер, в котором не добавлена поддержка этой функции, то вызов GetService<IServiceProviderIsService>() вернёт null.

Источники:
-
https://andrewlock.net/5-new-mvc-features-in-dotnet-7/
-
https://andrewlock.net/exploring-dotnet-6-part-10-new-dependency-injection-features-in-dotnet-6/
👍7
День 1416. #ЧтоНовенького
Новые Возможности MVC в .NET 7. Часть 3
В этой серии рассмотрим некоторые нововведения в ASP.NET Core в .NET 7.
Часть 1
Часть 2

3. Использование nullable-аннотаций
С каждым выпуском .NET поддержка ссылочных обнуляемых типов становится немного лучше. Для ASP.NET Core в .NET 7 она была добавлена в нескольких местах:

1) Обнуляемое тело запроса
В .NET 6 при привязке к телу запроса (независимо от того, используется ли явно [FromBody]), попытка привязки к запросу с пустым телом (где Content-Length == 0) завершится ошибкой "A non-empty request body is required." (Требуется непустое тело запроса).

Вы можете обойти это глобально в .NET 6 и разрешить null для параметра, настроив свойство MvcOptions.AllowEmptyInputInBodyModelBinding:
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<MvcOptions>(opts =>
{
opts.AllowEmptyInputInBodyModelBinding = true;
});

Другой вариант - включить его отдельно для каждого метода действия, добавив явный атрибут [FromBody] и установив EmptyBodyBehavior, например:
public IActionResult Post(
[FromBody(EmptyBodyBehavior=EmptyBodyBehavior.Allow)]
MyBody? body)
{
// body будет null при пустом теле
}
Это работает, но это слишком длинно и уродливо…

В .NET 7 всё становится намного проще. Вы можете полагаться на nullable-аннотации. Обнуляемый параметр подразумевает EmptyBodyBehavior.Allow, а необнуляемый - EmptyBodyBehavior.Disallow:
public IActionResult Post(MyBody? body)
{
// body будет null при пустом теле
}

public IActionResult Post(MyBody body)
{
// При пустом теле будет ошибка 400
}

2) Определение необязательности сервиса
В .NET 6, если вы не зарегистрировали тип сервиса в контейнере внедрения зависимостей, то, даже если декорированный атрибутом [FromService] параметр помечен как обнуляемый, попытка вызова конечной точки API приводит к ошибке:
InvalidOperationException: No service for type 'SomeService' has been registered (Сервис типа 'SomeService' не был зарегистрирован)

В .NET 7 обнуляемый параметр с атрибутом [FromServices] будет null, если он недоступен в DI:
public IEnumerable<MyClass> Get(
[FromServices] SomeService? service)
{ … }

Вам может быть интересно, что произойдёт, если вы попытаетесь привязать обнуляемый сервис, который не зарегистрирован в DI, используя автоматическое определение сервиса, описанное в части 2?
Ответ: скорее всего, не то, чего вы хотите. Если тип параметра (SomeService выше) не зарегистрирован в DI, он не будет рассматриваться как сервис, он будет рассматриваться как любой другой сложный тип. Это означает, что MVC попытается привязать его свойства к любым доступным параметрам: телу запроса, строке запроса, заголовкам и т.п.

Источник: https://andrewlock.net/5-new-mvc-features-in-dotnet-7/
👍4
День 1417. #юмор
В тему хайпа про ChatGPT.
👍11
День 1418. #Оффтоп
Сегодня порекомендую вам видео с канала Computerphile, которое называется «Horrible, Helpful, http3 Hack» https://youtu.be/wV9FSyFB8tk

HTTP3 вышел летом этого года, но создание его было сопряжено с некоторыми трудностями. В том числе пришлось использовать довольно кривой костыль. Ричард Дж. Клегг из Университета Королевы Мэри в Лондоне сначала напоминает про некоторые уровни сетевого взаимодействия, объясняет, что такое QUIC, и почему он не может определиться, любит он HTTP3 или ненавидит его.

Видео на английском, но доступны автоматические русские субтитры.
👍7
День 1419. #ЧтоНовенького
Новые Возможности MVC в .NET 7. Часть 4
В этой серии рассмотрим некоторые нововведения в ASP.NET Core в .NET 7.
Часть 1
Часть 2
Часть 3

4. Поддержка IResult
MVC имеет IActionResult и ActionResult<T>; минимальные API имеют IResult и IValueHttpResult<TValue>. И не стоит их путать!

К сожалению, это легче сказать, чем сделать. Если вы пишете много минимальных API, а затем погружаетесь в MVC, вы можете случайно написать что-то вроде этого:
[HttpGet]
public IResult Get()
{
return Results.Ok(
new { Name = "My name" });
}

К сожалению, технически это не ошибка: нет предупреждения во время компиляции об использовании IResult с MVC и нет ошибки во время выполнения. Вместо этого MVC сериализует объект IResult в JSON, что похоже на то, что вы хотите, пока вы не увидите вывод:
{
"value": {
"name" : "My name"
},
"statusCode" : 200,
"contentType" : null
}

Как видите, MVC не просто сериализует объект, который мы передали в Ok(), он сериализует весь объект Results. Это напоминает некоторые ужасные API, которые возвращают код состояния 200, но содержат в теле statusCode: 400!

В .NET 7 MVC добавили ограниченную поддержку сериализации объектов IResult, и, как и следовало ожидать, вместо результата выше вы получаете следующий вывод:
{
"name" : "My name"
}

Когда вы возвращаете IResult из метода действия MVC, вы не получаете никаких функций MVC, таких как согласование содержимого и средства форматирования вывода; вы всегда получаете JSON. Тем не менее, если вы создаёте API только с JSON, этот вариант имеет преимущество в том, что вы потенциально можете совместно использовать больше компонентов в ваших минимальных API и контроллерах MVC, просто возвращая IResult, вместо того чтобы реализовывать обработку как для IActionResult, так и для IResult.

Источник: https://andrewlock.net/5-new-mvc-features-in-dotnet-7/
👍8
День 1420. #Debugging
Я Исправил Ошибку. Что Дальше?
Чем больше кода вы пишете, тем больше ошибок создаёте. Т.е. как разработчик ПО вы должны тратить часть своего времени на отладку.

Есть несколько общих шагов при отладке приложения:
1. Получение всей необходимой информации для понимания проблемы (этапы воспроизведения, стек вызовов, журналы и т. д.)
2. Воспроизведение проблемы и отладка кода
3. Исправление кода

Однако мы можем сделать больше, чем просто решить конкретную проблему. Вот несколько вопросов, которые вы должны задать себе при исправлении ошибки.

1. Легко ли было получить информацию, необходимую для воспроизведения ошибки?
- Какую версию приложения запускает пользователь?
- Какова конфигурация среды? (ОС, версия .NET, текущая культура, разрешение экрана, ОЗУ, загрузка ЦП и т. д.)
- Какая конфигурация приложения? (Пользовательские настройки)
- В случае сбоя доступен ли стек вызовов и понятно ли сообщение об ошибке?
- Есть ли доступ к логам? Легко ли их получить и прочитать? Предоставляют ли они достаточно информации и контекста?
- Есть ли доступ к данным телеметрии?
На этом этапе у вас должно быть хорошее представление о проблеме, не глядя на код.

2. Легко ли воспроизвести проблему в среде разработки?
- Легко ли начать работу над проектом? (Получить код/Открыть проект в IDE/Начать отладку)
- Правильно ли задокументирован процесс?
- Нужно ли вручную настраивать секреты для подключения к внешним службам?
- Требуется ли дополнительное ПО на машине и как его получить?
- Легко ли настроить ту же среду, что и в рабочей среде (Azure Web App, Docker, Kubernetes)? Легко ли отлаживать эту среду?
- Можно ли получить анонимизированные живые данные, когда это необходимо?
- Если нельзя воспроизвести проблему в своей среде, можно ли вы отладить промежуточную/производственную версию или получить дамп? Будьте очень осторожны, когда делаете это, чтобы не заблокировать работающий сервис из-за достижения точки останова и не раскрыть важные данные.

3. Легко ли работать над кодовой базой?
- Хорошо ли организован код? Находите ли вы то, что ищете?
- Код легко читается? Соблюдается ли соглашение об именовании и стиль написания кода?
- Есть ли в коде неявные зависимости?
- Насколько быстро вы можете вносить изменения и наблюдать за их результатами в приложении?
- Можете ли вы воспользоваться своей IDE для отладки?
- Можете ли использовать точки останова и просматривать локальные значения или вычислять выражения?
- Ваши типы переопределяют ToString или отмечены атрибутом [DebuggerDisplay], чтобы вы могли быстро увидеть значения во время отладки?

4. Почему разработчик внёс эту ошибку в код?
На этом этапе вы должны найти причины, по которым разработчик допустил ошибку. Сделайте шаг назад и проанализируйте проблему.
- Код слишком сложный?
- Методы слишком длинные и слишком сложные?
- Используете ли вы правильный инструмент для выполнения работы?
- Возможна ли путаница? Например, два типа с одинаковым именем в разных пространствах имен.
- Ясно ли объясняются пред- и пост-условия метода (возможно ли передать/вернуть null, дату локальную/UTC, относительный/абсолютный путь?
- Отсутствует или неясна документация/комментарии?
- Влияет ли изменение части приложения на другую часть?
- Надёжны ли тесты?
- Есть ли хотя бы один сквозной тест для основного сценария приложения?

Источник: https://www.meziantou.net/how-to-correctly-fix-a-bug.htm
👍6
This media is not supported in your browser
VIEW IN TELEGRAM
День 1421. #ЧтоНовенького
Превью «Липкого» Скроллинга в VS 17.5
Писать длинные классы с многоуровневой вложенностью кода стало ещё приятнее в превью 2 Visual Studio 17.5. А всё благодаря новой функции «липкого скроллинга» (Sticky Scroll).

Она поддерживает контекст для кода, с которым вы работаете, сохраняя соответствующие заголовки. Когда вы прокручиваете код, пространства имён, классы и методы будут прилипать к верхней части редактора. См. видео. Помимо этого, нажатие мыши на заголовок приведёт к этой строке кода. Sticky Scroll поддерживает несколько форматов кода, включая C#, C++, XAML и JSON.

Sticky Scroll можно включить в меню Tools > Options > Text Editor > General > Sticky Scroll (Инструменты > Параметры > Текстовый редактор > Основные > Sticky Scroll). Там же вы можете установить максимальное количество строк, которые будут «прилипать».

Источник: https://devblogs.microsoft.com/visualstudio/sticky-scroll-now-in-preview/
👍20