Опытный разработчик не так давно зашёл в .Net и поставил цель получить сертификат Microsoft. Свой ежедневный прогресс он описывает на канале .Net Разработчик. Заметки об изученном материале, советы по повышению производительности и поддержке мотивации, ин
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/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.[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.htmlMemoryDiagnoser, позволяющий посмотреть информацию о потребляемой памяти и количестве сборок мусора.
Базовый случай
Иногда полезно обозначить базовый метод, относительно которого мы будем считать, насколько быстрее (или медленнее) работают остальные. Это можно сделать, добавив в атрибут 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/netdeveloper2022JRGpc даст скидку от 20% на билеты из категории «Для частных лиц».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/
ErrorMapper, ErrorCleaner или что-то подобное.
Это не только поставит логику на место, но также упростит контроллер и улучшит тестируемость: намного проще создать модульный тест метода, возвращающего ошибку и маппинга ошибок, чем тестировать целый контроллер, работающий с несколькими внепроцессными зависимостями.
Это, кстати, суть паттерна Humble Object. Идея состоит в том, чтобы извлечь важную часть логики (маппер ошибок) из контроллеров, чтобы упростить модульное тестирование этой логики.
Источник: https://khorikov.org/posts/2022-02-28-error-mapping/// Чего хотелось бы (не компилируется)
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.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/IsDeleted, чтобы указать, является ли запись/строка «удалённой» или неактивной, со значением false по умолчанию. Аналогично в NoSQL базе данных свойство IsDeleted указывает, удалён ли документ/объект.
Бизнес-правила
Работая с бизнес-логикой, мы очень редко задумываемся об удалении или мягком удалении чего-либо. Это связано с тем, что существуют бизнес-концепции и бизнес-процессы, не предусматривающие удаления данных. Специалисты в предметной области обычно не думают об «удалении» данных и не используют термин «удалить», когда говорят о бизнес-процессе или рабочем процессе.
Предположим, что мы работаем в системе управления персоналом. Сотрудник является ключевым аспектом системы, и вокруг него существуют различные рабочие процессы. Одним из таких понятий будет трудовой стаж. Когда сотрудника увольняют, мы не можем просто выполнить мягкое удаление и пометить его «Удалённым». Это не имеет смысла. У сотрудников будет жизненный цикл найма и увольнения.
События
Когда с сотрудником разрывается контракт, другие данные, вероятно, имеют отношение к этому событию. Когда контракт был подписан, когда разорван и по какой причине? Суть в том, что произошло бизнес-событие, и мы хотим сохранить все связанные с этим событием данные.
Документирование событий
Возможно, мы представляем это в нашем документе/объекте как набор событий найма и увольнения с соответствующими данными. Возможно, работник повторно нанят позже и имеет несколько периодов занятости.
Но ключевым моментом является фокус на происходящих бизнес-событиях. Если вы думаете о событиях как об основном драйвере вашей системы, вы, скорее всего, придёте к мысли о сохранении событий как состояния системы. Это называется Event Sourcing.
CRUD
Можно думать о системе в парадигме CRUD (Create-Read-Update-Delete). Однако в вашей предметной области, как уже упоминалось, вы не услышите, как люди говорят об «удалении». Почти все события имеют некоторый тип компенсирующего действия, которое завершает жизненный цикл процесса или «отменяет» или аннулирует предыдущее действие. Запись этих бизнес-событий и концепций является ключом к построению рабочего процесса в вашей системе. Если вы сосредоточены на CRUD, имейте в виду, что люди в предметной области мыслят бизнес-процессами и рабочими процессами. И ваша CRUD-система в этом случае представляет собой не более чем пользовательский интерфейс для базы данных без реальных возможностей.
GDPR
Можно возразить, что по закону о защите персональной информации может потребоваться удалить данные. Но суть не в этом.
Если вам нужно удалить данные, удалите их. Дело не в том, чтобы не удалять данные; дело в том, что при «мягком удалении» данных вы теряете информацию о бизнес-концепциях/событиях, которые, вероятно, произошли как часть бизнес-процесса или рабочего процесса. Произошедшие события и причины, по которым они произошли, могут быть невероятно ценными при построении надёжной системы, которая может развиваться по мере изменения требований.
Источник: https://codeopinion.com/should-you-soft-delete/SetUp). Можно использовать отдельные вспомогательные методы, поскольку это помогает избежать хранения состояния и улучшает читаемость.
Если вы сохраняете данные, проверьте, возможно, вам это и не нужно. Рассмотрите возможность использования моков, чтобы вместо проверки, например, были ли ваши данные записаны на диск, вы могли бы просто проверить, был ли вызван метод Write() фиктивного класса.
Но если избежать сохранения данных не удаётся, нужно очищать их в конце теста. Не стесняйтесь использовать вспомогательные функции системы тестирования, такие как TearDown.
Итого
Необходимость в конкретном порядке исполнения юнит-тестов — это код с душком. Есть лучшие способы убедиться, что данные находятся в желаемом состоянии, а также очистить эти данные перед запуском следующих тестов. Тем самым мы улучшаем жизнь себе будущему и нашим коллегам, а также повышаем нашу продуктивность.
Источник: https://intodot.net/unit-testing-best-practices-avoid-relying-on-test-order/