.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
День 1346.
Ребята из IT’s Tinkoff наконец-то выложили материалы сентябрьского митапа, который проходил в Нижнем Новгороде и на котором мне удалось побывать.

Поэтому сегодня предлагаю вашему вниманию доклады.

1. Николай Михалычев «Почему вам стоит начать вести документацию прямо сейчас»
Техническая документация — это та часть деятельности команды разработки, которой пренебрегают чаще всего, и которая первой же попадает под нож, когда нужно снижать стоимость или увеличивать скорость разработки. В то же время, правильно поставленная культура ведения документации позволит вам не только сэкономить время, но и сильно повысить качество вашего продукта. Николай поделился опытом нашей команды по выстраиванию процесса ведения документации, и тем, какие проблемы нам удалось решить с его помощью.
Доклад вызвал больше всего вопросов. И не все желающие успели их задать, т.к. уже крали время от следующего доклада. Но и потом в личной беседе Николая долго спрашивали о тонкостях процесса.

2. Кирилл Бажайкин «Микрооптимизации в .NET»
На примерах с бенчмарками и небольшим количеством теории Кирилл рассказал о микрооптимизации в .NET. Также объяснил почему этим не стоит заниматься, а если заниматься, как делать это правильно.
Наша любимая тема байто- и наносекундо- дрочерства. Автор привёл несколько полезных примеров, рассказал об утилитах мониторинга и анализа кода. Там и я включился с невнятным комментарием)))

3. Никита Сеньков «Неявное использование контравариантных постобработчиков в MediatR»
Никита рассмотрел использование абстракции IRequestPostProcessor для выполнения логики, применимой к нескольким типам команд. А также обсудил, откуда вообще растет такая задача, почему наивное решение не работает из коробки и, как починить это досадное недоразумение созданием еще одного метода расширения для регистрации компонентов MediatR.
Этот доклад можно, наверное, назвать наименее практически полезным из всех. Просто разбор довольно узкоспециализированной функциональности в нелюбимом многими медиаторе. Но знаю, что среди подписчиков есть люди, которые любят покопаться в таких кишочках и явить миру что-нибудь эдакое в виде решения. Тут я тоже влез с глупым вопросом. Умный вопрос пришёл в голову гораздо позже (как это обычно и бывает).

4. Руслан Артамонов «Межсервисные интеграции. Что может пойти не так?»
При написании бизнес-приложений каждый разработчик сталкивается с необходимостью обмениваться данными между сервисами, вызывать удаленные процедуры. В первом приближении такие задачи кажутся довольно тривиальными, пока речь не заходит о надежности.
Как быть до конца уверенным, что данные не потеряются, а удаленные процедуры выполнятся? Наша команда разрабатывает веб-приложение с большим количеством интеграций. В докладе Руслан поделился накопленным опытом и кейсами, а также рассмотрел ошибки, с которыми приходится сталкиваться и описал способы того, как можно их избежать.
👍16
День 1347. #юмор
👍8
День 1348. #Конференция
Команда Dotnetos широко известного в узких кругах дотнетчиков Конрада Кокосы с 10 по 12 октября проводит онлайн-конференцию.

Судя по расписанию докладов, нас ждёт много интересного.

Понедельник, 10 октября
1. 18:00 мск. Raffaele Rialdi «Preemptive monitoring applications in production»

2. 19:00 мск. Stefan Pölz «Never send a human to do a machine's job»

3. 20:00 мск. Miguel Angel Teheran «Getting started with OpenTelemetry in .NET»

Вторник, 11 октября
1. 18:00 мск. Timothy Barnett «A system's perspective – lessons from failures»

2. 19:00 мск. Victor Nicollet «ILPack : saving assemblies to disk»

3. 20:00 мск. Oren Eini «Scalable architecture from the ground up»

Среда, 12 октября
1. 18:00 мск. Rafał Schmidt «Your clients are lying! Adventure with leaky .NET»

2. 19:00 мск. Kevin Gosse «Pushing C# to new places with NativeAOT»

3. 20:00 мск. Krzysztof Stolarz «Road to .NET 6: migration story of hope and despair»

Ставим напоминалки и повышаем квалификацию!
👍12
День 1349. #ЧтоНовенького
Сравнение файлов в Visual Studio
Как разработчикам, нам часто приходится сравнивать два файла, чтобы найти различия. Иногда сравнивать содержимое буфера обмена с содержимым файла на диске, либо сравнивать наши локальные изменения с предыдущими версиями из Git.

Мэдс Кристенсен, известный разработчик многих расширений для Visual Studio, представил новое расширение File Differ как раз для этих целей.

После установки расширения в окне обозревателя решения нам будет доступен пункт всплывающего меню Compare (Сравнить) со следующими вариантами:
- Compare two files in Solution Explorer (Сравнить два файла из Обозревателя Решения),
- Compare file with another file on disks (Сравнить файл с другим файлом на диске),
- Compare file with content of clipboard (Сравнить файл с содержимым буфера обмена),
- Compare file with its unmodified version (Сравнить файл с неизменённой версией)
Также скоро будет доступен вариант Compare with Previous Version (Сравнить с предыдущей версией)

Эти варианты будут активны или неактивны в зависимости от контекста. Например, если вы выделите два файла в обозревателе решения, то будет доступен только пункт Compare > Selected files (Сравнить > Выбранные файлы). А если вы вызовете выпадающее меню на изменённом файле, находящемся под управлением системы контроля версий, будет доступен пункт Compare > File with Unmodified Version (Сравнить > Файл с Неизменённой версией).

Расширение работает и внутри редактора кода. Там также доступен пункт выпадающего меню Compare со следующими вариантами:
- Compare selection with clipboard (Сравнить выделение с содержимым буфера обмена),
- Compare active file with clipboard (Сравнить текущий файл с содержимым буфера обмена),
- Compare active file with saved version (Сравнить текущий файл с сохранённой версией)
- Compare active file with file on disk (Сравнить текущий файл с файлом на диске).

Заметьте, что здесь вам предлагается вариант сравнения несохранённой версии файла с тем, что сохранено на диске, в отличие от варианта в обозревателе решений, который сравнивает текущую локальную версию файла с версией в Git.

Расширение находится в стадии разработки и Мэдс призывает протестировать его и поделиться мыслями в репозитории на GitHub. Я, кстати, добавил туда одну идею. Мне не хватает возможности легко перемещаться по различиям и копировать различия из панели в панель по горячей клавише (напрмер, Alt+стрелка влево/вправо), без необходимости выделять, копировать и вставлять.

Источник: https://devblogs.microsoft.com/visualstudio/comparing-files-in-visual-studio/
👍7
День 1350. #Testing
Не Полагайтесь на Порядок Юнит-Тестов
Одна из распространённых ловушек, в которую может попасть новичок при написании юнит-тестов, — это положиться на их последовательное выполнение. Это ведь имеет смысл. В тестах можно ли создавать, читать и удалять некоторые данные, почему бы не использовать данные из прошлого теста?

Недостатки
1. Вы должны поддерживать состояние, и это очень плохо
Цель юнит-теста — подтвердить, что определённая часть кода выполняет свою работу правильно. Чтобы в этом убедиться, эту конкретную часть надо изолировать от всего остального. Поэтому, если мы полагаемся на то, что другой тест подготовит данные для нас, мы не следуем этому принципу.
Кроме того, очень сложно отслеживать входящие данные теста, если они не очевидны с первого взгляда. Представьте, что вы присоединяетесь к новой команде и должны выяснить, где генерируются данные для теста. Ужас!

2. Один новый тест может сломать все тесты
Что если нам надо добавить новый тест? Если в какой-то момент в тестах нам нужно иметь определённые данные, как мы можем быть уверены, что новый тест их не испортит? А что, если нужно удалить тест, который больше не нужен?

3. Невозможно использовать тест-кейсы
Это функциональность, которая позволяет вам запускать один и тот же тест несколько раз с разными параметрами. Поэтому, если вам нужно, чтобы ваш тест был подготовлен другим тестом, эта функция, вероятно, не будет совместима с вашими тестами.

4. Увеличивается время отладки
Если юнит-тест падает, мы не сможем сразу же запустить его снова, так как нужно будет запустить все остальные тесты перед ним, чтобы потом его можно было отладить. Если проблему трудно найти, может потребоваться много запусков. А возможно, проблема даже не в этом тесте, она может быть связана с каким-то другим тестом, который неправильно выполнил свою часть.

Как не полагаться на порядок юнит-тестов
В шаблоне Arrange Act Assert (AAA), все данные, необходимые для теста, должны быть подготовлены на этапе Arrange. Некоторые системы тестирования предлагают методы настройки (SetUp). Можно использовать отдельные вспомогательные методы, поскольку это помогает избежать хранения состояния и улучшает читаемость.

Если вы сохраняете данные, проверьте, возможно, вам это и не нужно. Рассмотрите возможность использования моков, чтобы вместо проверки, например, были ли ваши данные записаны на диск, вы могли бы просто проверить, был ли вызван метод Write() фиктивного класса.

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

Итого
Необходимость в конкретном порядке исполнения юнит-тестов — это код с душком. Есть лучшие способы убедиться, что данные находятся в желаемом состоянии, а также очистить эти данные перед запуском следующих тестов. Тем самым мы улучшаем жизнь себе будущему и нашим коллегам, а также повышаем нашу продуктивность.

Источник: https://intodot.net/unit-testing-best-practices-avoid-relying-on-test-order/
👍14
День 1351. #ЗаметкиНаПолях
Использовать ли Мягкое Удаление?
Удалять ли записи из базы данных или вместо этого использовать мягкое удаление?

На всякий случай, чтоб не было разногласий. Жёсткое удаление – это удаление записи из базы данных, т.к. вызов команды Delete в DBContext или SQL команды DELETE.

Как правило, если вы используете реляционную базу данных, у вас могут быть ограничения внешнего ключа, которые не позволяют вам удалять строки для обеспечения целостности данных. Мягкое удаление решает эту проблему. Чаще всего просто добавляется столбец IsDeleted, чтобы указать, является ли запись/строка «удалённой» или неактивной, со значением false по умолчанию. Аналогично в NoSQL базе данных свойство IsDeleted указывает, удалён ли документ/объект.

Бизнес-правила
Работая с бизнес-логикой, мы очень редко задумываемся об удалении или мягком удалении чего-либо. Это связано с тем, что существуют бизнес-концепции и бизнес-процессы, не предусматривающие удаления данных. Специалисты в предметной области обычно не думают об «удалении» данных и не используют термин «удалить», когда говорят о бизнес-процессе или рабочем процессе.

Предположим, что мы работаем в системе управления персоналом. Сотрудник является ключевым аспектом системы, и вокруг него существуют различные рабочие процессы. Одним из таких понятий будет трудовой стаж. Когда сотрудника увольняют, мы не можем просто выполнить мягкое удаление и пометить его «Удалённым». Это не имеет смысла. У сотрудников будет жизненный цикл найма и увольнения.

События
Когда с сотрудником разрывается контракт, другие данные, вероятно, имеют отношение к этому событию. Когда контракт был подписан, когда разорван и по какой причине? Суть в том, что произошло бизнес-событие, и мы хотим сохранить все связанные с этим событием данные.

Документирование событий
Возможно, мы представляем это в нашем документе/объекте как набор событий найма и увольнения с соответствующими данными. Возможно, работник повторно нанят позже и имеет несколько периодов занятости.

Но ключевым моментом является фокус на происходящих бизнес-событиях. Если вы думаете о событиях как об основном драйвере вашей системы, вы, скорее всего, придёте к мысли о сохранении событий как состояния системы. Это называется Event Sourcing.

CRUD
Можно думать о системе в парадигме CRUD (Create-Read-Update-Delete). Однако в вашей предметной области, как уже упоминалось, вы не услышите, как люди говорят об «удалении». Почти все события имеют некоторый тип компенсирующего действия, которое завершает жизненный цикл процесса или «отменяет» или аннулирует предыдущее действие. Запись этих бизнес-событий и концепций является ключом к построению рабочего процесса в вашей системе. Если вы сосредоточены на CRUD, имейте в виду, что люди в предметной области мыслят бизнес-процессами и рабочими процессами. И ваша CRUD-система в этом случае представляет собой не более чем пользовательский интерфейс для базы данных без реальных возможностей.

GDPR
Можно возразить, что по закону о защите персональной информации может потребоваться удалить данные. Но суть не в этом.
Если вам нужно удалить данные, удалите их. Дело не в том, чтобы не удалять данные; дело в том, что при «мягком удалении» данных вы теряете информацию о бизнес-концепциях/событиях, которые, вероятно, произошли как часть бизнес-процесса или рабочего процесса. Произошедшие события и причины, по которым они произошли, могут быть невероятно ценными при построении надёжной системы, которая может развиваться по мере изменения требований.

Источник: https://codeopinion.com/should-you-soft-delete/
👍10
День 1352. #ЗаметкиНаПолях
Проверка Порядка Выполнения с Помощью Юнит-тестов
Прежде всего, давайте проясним: этот пост не о порядке выполнения юнит-тестов. Никогда не следует рассчитывать на то, что тесты выполняются в каком-либо порядке. Мы же поговорим о проверке порядка выполнения операций внутри определённого метода с помощью тестов. Иногда вам нужно сделать больше, чем просто проверить правильность вызова методов.

Предположим, что мы реализуем сервис-оркестратор, который должен выполнить работу, а затем сообщить об исполнении. Хорошо бы, чтоб уведомление отсылалось после выполнения работы.

Для этого мы можем воспользоваться функцией обратного вызова Moq. Она позволяет нам выполнить код сразу после вызова метода. С её помощью мы можем установить флаг, чтобы отслеживать, когда был вызван метод, а затем использовать этот флаг, чтобы проверить, был ли второй метод вызван в нужное время:
public class JobService
{
private IWorkService ws;
private INotifyService ns;

// внедряем сервисы через DI

public void DoAndNotify()
{
ws.DoJob();
ns.Notify();
}
}

А вот наш тест:
[Test]
public void ShouldNotifyAfterJob()
{
var wsMock = new Mock<IWorkService>();
var nsMock = new Mock<INotifyService>();
var sut = new JobService(
wsMock.Object,
nsMock.Object);

var jobExecuted = false;
var notified = false;
wsMock.Setup(x => x.DoJob())
.Callback(() => jobExecuted = true);
nsMock.Setup(x => x.Notify())
.Callback(() => notified = jobExecuted);

sut.DoAndNotify();

Assert.That(notified, Is.True);
}

Что тут происходит:
- Создаём моки зависимостей и сервис.
- Устанавливаем функцию обратного вызова для метода DoJob, поэтому при его вызове флаг jobExecuted будет установлен в true.
- Устанавливаем функцию обратного вызова для метода Notify, в котором флаг notified будет истинным только, если jobExecuted имеет значение true.

Итого
Всего в паре строк кода мы проверяем порядок выполнения с помощью юнит-тестов. Более того, для этого нам даже не пришлось изменять наш код. Аналогично можно проверять и более сложные случаи.

Источник: https://intodot.net/verifying-the-execution-order-with-unit-tests/
👍13
LINQ.pdf
3.6 MB
День 1353. #Книги
Сегодня предложу вашему вниманию небольшую книгу Стивена Жизеля «LINQ Explained with sketches» (Объяснение LINQ в зарисовках). Я уже где-то встречал отрывки из этой книги в сети, здесь же они собраны воедино.

Книга хорошо структурирована и содержит отличные пояснения со схемами и примерами на C#, которые помогают чётко понять суть метода и как им пользоваться. Подойдёт как новичкам для изучения LINQ, так и опытным разработчикам в качестве шпаргалки.
👍31
День 1354. #юмор
Все мы иногда этот ИИ.
👍29
День 1355. #Карьера #ВопросыНаСобеседовании
Что Отвечать на Вопрос о Зарплатных Ожиданиях
Наткнулся на интересное видео «Пример лучшего ответа на вопрос о ваших зарплатных ожиданиях». Автор представляет дословный скрипт, куда нужно подставить только цифры. Это мне показалось интересным, поэтому решил «локализовать» для нашей части света.

Для начала несколько советов.
1. Изучите диапазон средних зарплат на вашу должность перед тем, как пойдёте на собеседование.
Данные можно взять, например с Хабр.Карьеры (для РФ) или DOU (для Украины). Добавьте в комментариях, если знаете другие источники.

2. Просите зарплату слегка ниже верхнего предела.
К примеру, если средняя ЗП на уровень мидл разработчика находится в диапазоне от $2000 до $3500, просите $3200.

3. Обязательно поясните, почему вы достойны такой зарплаты.

Теперь собственно скрипт:
«Перед тем, как пойти на собеседование я исследовал средние зарплаты по рынку на эту должность. Средние зарплаты колеблются от $2000 до $3500. Несмотря на то, что я хотел бы получать больше, я понимаю, что вы меня не знаете, а мне ещё нужно доказать, что я достоин зарабатывать такие деньги. Учитывая это, комфортным для меня уровнем будет $3200, и я уверен, что моё трудолюбие, ответственность, исполнительность и способность быстро вникать в рабочие процессы докажут вам, что я стою потраченных на меня денег.»

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

Что вы отвечали на вопрос о зарплатных ожиданиях и дали ли вам требуемую сумму? Поделитесь в комментариях.

Источник: https://youtu.be/0k13HVHJoNc
👍8
День 1356. #ЗаметкиНаПолях #AsyncTips
Асинхронные Свойства

Задача:
имеется свойство, которое вам хотелось бы объявить, как асинхронное. Свойство не задействовано в связывании данных.

Решение
Эта проблема часто встречается при преобразовании существующего кода для использования async; в таких ситуациях создаётся свойство, get-метод которого вызывает асинхронный метод. Вообще, такого понятия, как «асинхронное свойство», не существует. Ключевое слово async не может использоваться со свойством, и это хорошо. Get-методы свойств должны возвращать текущие значения; они не должны запускать фоновые операции:
// Чего хотелось бы (не компилируется)
public int Data
{
async get
{
await Task.Delay(TimeSpan.FromSeconds(1));
return 13;
}
}

Когда вам кажется, что вам нужно «асинхронное свойство», в действительности требуется нечто иное. Если ваше «асинхронное свойство» должно запускать новое (асинхронное) вычисление каждый раз, когда оно читается, то, по сути, это замаскированный метод:
public async Task<int> GetDataAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
return 42;
}

Вы можете получить Task<int> непосредственно из свойства:
public Task<int> Data
{
get { return GetDataAsync(); }
}

И все же так делать не рекомендуется. Это свойство должно стать методом. Так это ясно показывает, что каждый раз запускается новая асинхронная операция, поэтому API не вводит пользователя в заблуждение.

Стоит задуматься над тем, как состояние соотносится с асинхронным кодом. Это особенно актуально при преобразовании синхронной кодовой базы в асинхронную. Возьмем любое состояние, доступ к которому осуществляется через API (например, через свойства). Для каждой составляющей состояния спросите себя: что считать текущим состоянием объекта с незавершенной асинхронной операцией? Правильного ответа не существует, но важно продумать то, какие семантики вам нужны и как их документировать.

Для примера возьмем объект Stream.Position, представляющий текущее смещение указателя в потоке. С синхронным API при вызове Stream.Read или Stream.Write чтение/запись завершается, а Stream.Position обновляется новой позицией перед возвращением управления методом Read или Write. Для синхронного кода семантика ясна.

Теперь возьмем Stream.ReadAsync и Stream.WriteAsync: когда должно обновляться значение Stream.Position? При завершении операции чтения/записи или до того, как это фактически произойдет? Если оно обновляется перед завершением операции, то будет ли оно обновлено синхронно к моменту возвращения управления ReadAsync/WriteAsync или же вскоре после этого?

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

Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
👍12
День 1357. #ЗаметкиНаПолях #DDD
Маппинг Ошибок. Чья Это Ответственность?
Допустим, модель предметной области возвращает ошибку, о которой нельзя сообщать пользователю, поэтому её нужно смаппить в дружелюбное пользователю сообщение или просто очистить от конфиденциальных данных. Должно ли это происходить на уровне домена или в контроллере (на прикладном уровне)?

Кажется, что «чище» поместить этот код в контроллер, так как это зона ответственности контроллера. С другой стороны, это похоже на проблему домена.

Это довольно типичный случай. Самый распространенный пример: приложение проверяет имя пользователя и пароль при входе в систему. В этом сценарии может быть несколько ошибок проверки, в том числе:
- пользователь не найден в базе;
- имя пользователя не соответствует основным правилам проверки (например, оно слишком короткое);
- пароль неверный и т.д.

Из соображений безопасности вы не можете отобразить пользователю точную ошибку проверки. Все, что вы можете показать, это что-то обобщённое: «Имя пользователя или пароль неверны». Где должно происходить сопоставление конкретной ошибки с обобщённым сообщением? На уровне домена или приложения?

Это должно происходить на уровне домена.

Маппинг/очистка сообщения об ошибке является проблемой предметной области. Это исходит из бизнес-требований, что означает, что должно быть частью домена. Введите отдельный класс предметной области, такой как ErrorMapper, ErrorCleaner или что-то подобное.

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

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

Источник: https://khorikov.org/posts/2022-02-28-error-mapping/
👍20
День 1358. #Testing #Benchmark
Руководство по Бенчмарку в .NET. Начало
В этой серии постов рассмотрим, как создать проект .NET для тестирования производительности.

Начнём с чистого листа и создадим проект. Мы протестируем два метода, которые разными способами объединяют строки. Для этого будем использовать пакет BenchmarkDotNet.

Если вы, как я, не можете запомнить структуру проекта с бенчмарком и постоянно её гуглите, можно просто установить шаблоны:
dotnet new install BenchmarkDotNet.Templates

Теперь создадим проект, используя шаблон:
dotnet new benchmark --console-app -f net6.0 -o StringBenchmarks

Здесь мы используем несколько флагов:
--console-app – консольное приложение,
-f net6.0 – целевой фреймворк .NET 6.0,
-o StringBenchmarks – название проекта и папки для него.

Шаблон создаёт 2 класса:
- Benchmarks, в который добавляет 2 метода для тестовых сценариев: Scenario1 и Scenario2, помеченные атрибутом [Benchmark],
- Program, который собственно запускает бенчмарк.

Назовём методы удобными именами и добавим простые методы объединения строк: через конкатенацию и через StringBuilder.
namespace StringBenchmarks;

public class Benchmarks
{
[Benchmark]
public string StringJoin()
{
return string.Join(", ",
Enumerable.Range(0, 10)
.Select(i => i.ToString()));
}

[Benchmark]
public string StringBuilder()
{
var sb = new StringBuilder();
for (int i = 0; i < 10; i++)
{
sb.Append(i);
sb.Append(", ");
}

return sb.ToString();
}
}
Да, метод со StringBuilder выведет лишнюю запятую. Мы к этому ещё вернёмся. Запускаем проект:
dotnet run -c Release

Здесь стоит отметить, что должна использоваться конфигурация Release, чтобы максимально оптимизировать сборку. Кроме того, если вы хотите, чтобы тесты были максимально точными, закройте все работающие приложения и запустите проект из терминала.

Бенчмарк выдаст подробный отчёт. В начале выдаётся информация о системе, в которой проходили тесты, а затем собственно результаты с разными статистическими подробностями. Мы обратим внимание на колонку Mean (Среднее):
|        Method |     Mean |
|-------------- |---------:|
| StringJoin | 69.71 ns |
| StringBuilder | 41.19 ns |

Как видите, метод, использующий StringBuilder, работает заметно быстрее.

Продолжение следует…

Источник:
https://blog.nimblepros.com/blogs/benchmarking-in-dotnet/
👍13
Иллюзия идеального выбора lock(_sync), троттлинг запросов, различия в мышлении инженера и архитектора — обо всем этом поговорим на DotNext 2022 Autumn.

Конференция пройдет 3–4 ноября в онлайне и 20 ноября в офлайне.

В программе уже есть первые доклады. Среди них:
✔️ Станислав Сидристый (ЦРТ) — «lock(_sync): иллюзия идеального выбора».
✔️ Евгений Пешков (Тинькофф) — «Алгоритмы троттлинга запросов».
✔️ Дмитрий Сошников (МАИ / НИУ ВШЭ) — «Как научить вашего ребенка программировать (и не только)».
✔️ Дмитрий Таболич (ИТ1) — «Думай как архитектор: майндшифт инженера».

Участников ждут тематические дискуссии с другими участниками, интервью с экспертами, доклады от партнеров и много других активностей.

Подробности и билеты — dotnext.ru

Если вам хочется на несколько часов отвлечься и побыть среди единомышленников, то приходите на DotNext. А промокод netdeveloper2022JRGpc даст скидку от 20% на билеты из категории «Для частных лиц».
👍5👎1
День 1359. #Testing #Benchmark
Руководство по Бенчмарку в .NET. Продолжение
Начало

Мы можем выводить дополнительную информацию в отчёте с помощью атрибутов.

Память
Наверное, самым популярным является MemoryDiagnoser, позволяющий посмотреть информацию о потребляемой памяти и количестве сборок мусора.

Базовый случай
Иногда полезно обозначить базовый метод, относительно которого мы будем считать, насколько быстрее (или медленнее) работают остальные. Это можно сделать, добавив в атрибут Benchmark параметр Baseline=true.

Параметры
Можно посмотреть, как себя ведёт метод в зависимости от объёма данных. В этом поможет атрибут Params(…), в который надо передать массив размеров входных данных.

Среда исполнения
Мы можем сравнить быстродействие кода в разных средах исполнения. Например, .NET 6.0 с .NET 7.0. Для этого используется атрибут SimpleJob(RuntimeMoniker.…). Убедитесь, что у вас установлена версия BetnchmarkDotNet 0.13 или выше, чтобы тестировать в средах .NET 5.0 и выше. Также убедитесь, что все эти среды исполнения установлены на вашей машине.

Собираем всё вместе:
namespace StringBenchmarks {
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net50)]
[SimpleJob(RuntimeMoniker.Net60, baseline: true)]
[SimpleJob(RuntimeMoniker.Net70)]
public class Benchmarks
{
[Params(5, 50, 500)]
public int N { get; set; }

[Benchmark(Baseline = true)]
public string StringJoin()
{
return string.Join(", ",
Enumerable.Range(0, N)
.Select(i => i.ToString()));
}

[Benchmark]
public string StringBuilder()
{
var sb = new StringBuilder();
for (int i = 0; i < N; i++)
{
sb.Append(i);
sb.Append(", ");
}

return sb.ToString();
}
}
}

Мы добавили параметр N, которому бенчмарк задаст значения 5, 50 и 500 соответственно в разных тестах. Также мы запустим тесты в 3х средах исполнения: .NET 5.0, .NET 6.0 (базовая среда) и .NET 7.0. Кроме того, добавлена диагностика памяти и за базовый случай взят метод StringJoin.

Результаты приведены на картинке ниже. Из результатов, например, заметно, что метод, использующий StringBuilder, с каждой новой версией .NET работает всё быстрее.

Не обязательно просматривать результаты в консоли. BenchmarkDotNet выводит результаты в папку BenchmarkDotNet.Artifacts. Там будут файлы отчетов в форматах html, csv и markdown. Это может быть очень полезно для добавления в PR или комментарий к релизу на Github или других подобных платформах.

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

Источник:
https://blog.nimblepros.com/blogs/benchmarking-in-dotnet/
👍13
День 1360. #Testing #Benchmark
Руководство по Бенчмарку в .NET. Окончание
Начало
Продолжение

Настройка и очистка
Иногда нужно написать какую-то логику, которая должна выполняться до или после бенчмарка, но мы не хотим, чтобы она участвовала в бенчмарке.
- Метод, помеченный атрибутом [GlobalSetup], будет выполняться только один раз для тестируемого метода после инициализации параметров бенчмарка и до всех вызовов тестового метода.
- Метод, помеченный атрибутом [GlobalCleanup], будет выполняться только один раз для тестируемого метода после всех вызовов тестового метода.
- Метод, помеченный атрибутом [IterationSetup], будет выполняться ровно один раз перед каждым вызовом теста. Не рекомендуется использовать его в микробенчмарках, так как это может испортить результаты. Однако, он может быть полезен, если тест занимает не менее 100 мс, и вы хотите подготовить некоторые данные перед каждым вызовом.
- Метод, отмеченный атрибутом [IterationCleanup], будет выполняться ровно один раз после каждого вызова. Этот атрибут также не рекомендуется использовать в микробенчмарках.

Валидация
Итак, бенчмарк позволяет сравнить между собой производительность методов, которые, по идее, должны делать одно и то же. Однако, если внимательно присмотреться к коду из первой части, можно заметить, что это не так. Метод StringBuilder() выводит лишнюю запятую в конце, в отличие от метода StringJoin(). Эта конкретная ошибка вряд ли сильно повлияла на результаты бенчмарка, но нам может и не повезти с этим в следующий раз.

Хотелось бы иметь какой-то автоматизированный тест, подтверждающий, что результат работы методов одинаковый.

BenchmarkDotNet умеет и это. Всё, что нам нужно сделать, это добавить ReturnValueValidator в наш тестовый класс, и все готово.

[ReturnValueValidator(failOnError: true)]
public class Benchmarks
{
// остальной код скрыт для краткости
// см. предыдущий пост
}

Теперь при попытке запуска нашего бенчмарка, если методы не возвращают одинаковый результат, мы получим ошибку:
// Validating benchmarks (Проверка бенчмарков):
Inconsistent benchmark return values in Benchmarks (Несогласованные возвращаемые значения в бенчмарках): StringJoin: 0, 1, 2, 3, 4, StringBuilder: 0, 1, 2, 3, 4,

* Здесь значения кажутся одинаковыми, но на самом деле это из-за того, что BenchmarkDotNet ставит запятую в сообщении об ошибке после результата первого метода. На самом деле это следует читать как:
StringJoin: "0, 1, 2, 3, 4", StringBuilder: "0, 1, 2, 3, 4,"

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

Также существует BaselineValidator, который проверяет, что параметр Baseline=true атрибута Benchmark добавлен только в одном методе. Этот валидатор обязательный.

JitOptimizationsValidator проверяет, все ли зависимости проекта оптимизированы. По умолчанию отключен.

ExecutionValidator проверяет, могут ли исполниться все бенчмарки, исполняя все методы по одному разу перед запуском тестов. Если в каком-то из методов возникает ошибка, бенчмарки не запускаются. По умолчанию отключен.

Источники:
-
https://blog.nimblepros.com/blogs/validating-benchmarks/
-
https://benchmarkdotnet.org/articles/features/setup-and-cleanup.html
-
https://benchmarkdotnet.org/articles/configs/validators.html
👍16
День 1361. #TipsAndTricks
Управление Директивами Using в Visual Studio
В последней версии Visual Studio вам не нужно управлять директивами using вручную. VS может добавлять/удалять их автоматически. Сегодня рассмотрим, как настроить Visual Studio для автоматической обработки директив using.

Все настройки будут выполняться в окне Options (Tools > Options…).

1. Управление директивами
Для начала посмотрим на опции, которые есть уже давно. Перейдите в раздел Text Editor > C# > Advanced (Текстовый редактор > C# > Дополнительно). Прокрутите вниз до блока Using Directives (Директивы Using). Здесь вы можете настроить:
- сортировку (располагать директивы System вверху),
- разделение групп директив,
- автоматическое добавление using при вставке кода,
- включить или выключить предложения добавления директив using для пространств имён из .NET Framework или из NuGet пакетов.

2. Автоматическое сворачивание
Перейдите в раздел Text Editor > C# > Advanced (Текстовый редактор > C# > Дополнительно). Прокрутите вниз до блока Outlining (Обрисовка), отметьте пункт Collapse usings on file open (Сворачивать директивы using при открытии файла).

3. Добавление в IntelliSense
Visual Studio может отображать типы из неимпортированных пространств имён в IntelliSense. Перейдите в раздел Text Editor > C# > IntelliSense (Текстовый редактор > C# > IntelliSense). Прокрутите в самый низ и отметьте пункт Show items from unimported namespaces (Показывать элементы из неимпортированных пространств имён). Теперь при наборе кода во всплывающей подсказке будут показываться все доступные типы. Для неимпортированных пространств имён справа будет показываться его имя. При выборе элемента Visual Studio автоматически добавит директиву using, если это необходимо.

4. Очистка неиспользуемых директив
Если вы хотите очищать неиспользуемые директивы using, вы можете настроить Visual Studio для запуска очистки кода при сохранении изменений. Перейдите в раздел Text Editor > Code Cleanup (Текстовый редактор > Очистка Кода). Отметьте пункт Run Code Cleanup on Save (Выполнять Очистку Кода при Сохранении). Затем выберите профиль очистки и нажмите на ссылку Configure Code Cleanup (Настроить Очистку Кода). В новом окне из нижнего списка Available fixers (Доступные фиксеры) выберите Remove unnecessary Imports or usings (Убрать ненужные директивы Import или using), а также любые другие, которые вам могут понадобиться. Сохраните изменения. Теперь при каждом сохранении файла будет производиться очистка кода и в том числе удаление ненужных директив.

Источник: https://www.meziantou.net/configuring-visual-studio-to-handle-using-directives-automatically.htm
👍14
День 1362. #ЗаметкиНаПолях #AsyncTips
Асинхронное освобождение

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

Решение
Есть два распространённых варианта действий.

1. Запрос на отмену всех текущих операций.
Такие типы, как файловые потоки и сокеты, отменяют все существующие операции чтения и записи при закрытии. Определив собственный CancellationTokenSource и передавая этот маркер внутренним операциям, можно сделать нечто похожее. В этом случае Dispose отменит операции, не ожидая их завершения:
class MyClass : IDisposable
{
private readonly CancellationTokenSource _сts =
new CancellationTokenSource();

public async Task<int> CalcAsync()
{
await Task.Delay(
TimeSpan.FromSeconds(2),
_сts.Token);

return 42;
}

public void Dispose()
{
_сts.Cancel();
}
}

Выше приведён упрощённый код. В реальном паттерне Dispose не всё так просто. А также стоит предоставить пользователю возможность передать собственный маркер CancellationToken (используя приём, описанный в этом посте).

При вызове Dispose будут отменены все существующие операции в вызывающем коде:
async Task UseMyClassAsync()
{
Task<int> task;
using (var resource = new MyClass())
{
task = resource.CalcAsync(default);
}
// Выдает OperationCanceledException.
var result = await task;
}
Для некоторых типов (например, HttpClient) такая реализация работает вполне нормально. Однако иногда необходимо убедиться, что будут завершены все операции.

2. Асинхронное освобождение впервые появилось в C# 8.0. Появились интерфейс IAsyncDisposable и команда await using. Таким образом, типы, которые собирались выполнить асинхронную работу при освобождении, теперь получили такую возможность:
class MyClass : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(TimeSpan.FromSeconds(2));
}
}

Использование:
await using (var myClass = new MyClass())
{

} // <--
// Здесь вызывается DisposeAsync (с ожиданием)

Также можно использовать ConfigureAwait(false):
var myClass = new MyClass();
await using (myClass.ConfigureAwait(false))
{

} // <--
// Здесь вызывается DisposeAsync (с ожиданием)
// с ConfigureAwait(false).

Асинхронное освобождение определенно проще, а первый подход должен использоваться только в том случае, если это действительно необходимо. Также при желании можно использовать оба подхода одновременно. Это наделит ваш тип семантикой «безопасного завершения работы», если в клиентском коде используется await using, и семантикой «жёсткой отмены», если клиентский код использует Dispose.

Источник: Стивен Клири “Конкурентность в C#”. 2-е межд. изд. — СПб.: Питер, 2020. Глава 11.
👍11
День 1363. #ЗаметкиНаПолях
CQRS. Факты и Мифы. Начало
Технические паттерны полны мифов и неверных толкований. Довольно часто это происходит с CQS и CQRS. Часто про CQ(R)S можно услышать, что:
- нужны две базы данных,
- нужно использовать очередь сообщений (например, RabbitMQ или Kafka),
- это сложно применить и усложняет архитектуру,
- возникнут проблемы окончательной согласованности (Eventual Consistency),
- нужно реализовывать паттерн Источников Событий (Event Sourcing).

CQS означает Разделение Команд и Запросов (Command Query Separation).
CQRS — Разделение Ответственности Команд и Запросов (Command Query Responsibility Segregation).

Ничто в названии не говорит о двух базах данных, разных таблицах или вообще о том, как информация хранится. Зато и в CQS, и в CQRS в названии есть Команды и Запросы:
Команда (Command) — это запрос на изменение.
Запрос (Query) — это запрос на возврат данных.

Команда для добавления события может выглядеть так:
public class CreateEvent
{
public string Name { get; }
public string Where { get; }
public DateTime When { get; }
}

Запрос на получение данных события может выглядеть так:
public class GetEvent
{
public Guid Id { get; }
}

Как видите, это простые DTO.

Шаблон CQS был создан Бертраном Мейером во время его работы над языком Eiffel. Он утверждал, что: «Задавание вопроса не должно менять ответ» и определил, что: «Команда (процедура) что-то делает, но не возвращает результат. Запрос (функция) возвращает результат, но не изменяет состояние». Благодаря такому различию обработку можно сделать более простой и предсказуемой. Запрос не создаст никаких побочных эффектов. Команда не будет использоваться для получения данных.

CQRS является расширением CQS. Грег Янг определил его так: «Разделение ответственности команд и запросов использует то же определение команд и запросов, что и у Мейера, и придерживается точки зрения, что они должны быть чистыми. Принципиальное отличие состоит в том, что в CQRS объекты делятся на два типа, один из которых содержит команды, а другой — запросы». Таким образом, CQS определяет общий принцип поведения. CQRS более конкретно говорит о реализации.

При использовании CQRS должно быть строгое разделение между моделью записи и моделью чтения. Они должны обрабатываться отдельными объектами (обработчиками) и не быть концептуально связанными друг с другом. Обработчики же не являются структурами хранения информации и не связаны с тем, где и как будут храниться данные. Обработчики команд исполняют команды, которые изменяют состояние или выполняют другие операции с побочными эффектами. Обработчики запросов отвечают за возврат результата запроса.

Ничто не мешает модели записи и модели чтения иметь одинаковую структуру или использовать одни и те же таблицы БД. Более того, в CQRS не обязательно использовать базу данных. Под капотом может быть Excel, текстовый файл или внешний API. Самое главное — концептуально разделять модели записи и чтения данных.

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

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

Источник:
https://event-driven.io/en/cqrs_facts_and_myths_explained/
👍21
День 1364. #ЗаметкиНаПолях
CQRS. Факты и Мифы. Окончание
Начало

Теперь перейдём к разбору мифов.

1. Усложняет ли CQRS архитектуру?
В архитектуре CQRS вы делите свою модель и API на вертикальные слои. Каждый обработчик команд/запросов представляет собой отдельный кусок, отдельную единицу кода (новый обработчик можно создавать даже через копирование/вставку). Благодаря этому вы можете настроить конкретный метод, не следуя общим соглашениям (например, использовать чистый SQL-запрос или даже другое хранилище). В традиционной многоуровневой архитектуре изменение базового универсального механизма на одном уровне может повлиять на все методы. Но мир не идеален: иногда исключений больше, чем случаев, подпадающих под общее правило. Вертикальное разделение помогает разработчику сосредоточиться на конкретной бизнес-функции, не отвлекаясь на остальную логику приложения.

2. Почему считается, что нужно иметь разные таблицы или базы данных?
CQRS позволяет настроить модель запроса в соответствии с потребностями клиентов. Типичным случаем является наличие отдельной модели чтения для каждого представления. Вам не нужны все детали объекта, если вы просто отображаете краткую сводку. Такие модели чтения могут представлять собой слегка отличающиеся SQL-запросы, выбирающие другой диапазон столбцов из таблицы. Но они также могут быть материализованными представлениями или отдельными таблицами. Можно сделать это для оптимизации производительности, чтобы записи и запросы не влияли друг на друга. Если у вас такой случай, то одно из возможных решений — использовать разные таблицы или базы данных и синхронизировать их после записи. Однако это не общее правило. Нужно выбрать стратегию, которая соответствует потребностям.

3. Откуда возникла необходимость в очередях обмена сообщениями?
CQRS позволяет иметь разные хранилища для разных бизнес-кейсов. Например, реляционную базу данных в модели записи и базу данных документов в модели чтения (например, Elastic Search для полнотекстового поиска) или любую другую комбинацию. Создание модели чтения в этом случае должно иметь логику преобразования. У вас могут быть отдельные сервисы, отвечающие за бизнес-логику по изменению состояния и за постройку моделей чтения. Очереди обмена сообщениями (например, RabbitMQ, Kafka) могут помочь синхронизировать их, уведомляя процессоры модели чтения о новом изменении в модели записи. Но можно начать с малого, используя таблицы и представления базы данных или очереди в памяти.

4. Что насчёт окончательной согласованности?
Если вы используете очереди сообщений или несколько баз данных, данные из хранилища для записи копируются (и преобразуются) в хранилище для чтения асинхронно. В результате хранилище чтения отстает от хранилища записи и имеет место Окончательная Согласованность (Eventual Consistency). Даже материализованные представления или репликация в базе данных могут иметь окончательную согласованность.
Когда изменение, внесённое в модель записи, влияет на несколько моделей чтения в одной и той же транзакции, стоит подумать о том, является ли влияние на производительность значительным. Один из возможных подходов — выгрузить обновление в фоновый асинхронный процесс. Зная, какие изменения были внесены, вы можете обрабатывать их по одному или, например, пакетами в процессе ETL.

5. Нужен ли Event Sourcing?
Нет. Event Sourcing довольно часто отождествляют с CQRS. Он по определению имеет модели Write и Read. События сохраняются в журнале только для добавления. Они являются источником истины. Модели чтения создаются и обновляются на основе событий. Event Sourcing и CQRS подходят друг другу, однако это разные парадигмы. Event Sourcing — это «всего лишь» один из вариантов, который вы можете выбрать в качестве реализации хранилища.

Источник: https://event-driven.io/en/cqrs_facts_and_myths_explained/
👍6