.NET Разработчик
6.68K subscribers
451 photos
4 videos
14 files
2.17K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

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

Начало
Продолжение

Детерминированные и недетерминированные обработчики
Что произойдёт, когда ваш обработчик вызывает что-то вне базы данных: API, отправка email, платёжный шлюз или очередь фоновых заданий? Всё это распространённые побочные эффекты, которые также должны быть идемпотентными.

Эти вызовы находятся за пределами транзакции. БД может успешно завершить транзакцию, но, если сеть перестанет работать до ответа внешнего сервиса, вы не сможете определить, произошло действие или нет. При повторной попытке клиент может отправить ещё один email или дважды списать средства с карты.

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

1. Использовать ключ идемпотентности во внешнем вызове
Если внешний сервис это поддерживает, передавайте стабильный идентификатор, например, MessageId сообщения, с каждым запросом. Многие API, включая платёжные системы и платформы email, позволяют указывать заголовок c ключом идемпотентности. Сервис гарантирует, что идентичные запросы с одним и тем же ключом будут выполнены только один раз:
await emailSvc.SendAsync(new EmailRequest
{
To = user.Email,
Subject = "Привет!",
Body = "Спасибо за подписку на канал.",
IdempotencyKey = ctx.MessageId
});

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

2. Сохраните намерение локально
Если внешний сервис не поддерживает ключи идемпотентности, вы можете сымитировать его. Сохраните запись о предполагаемом действии в базе перед вызовом внешней системы. Например, таблица PendingEmails, содержащая информацию о сообщениях на отправку.
Фоновый процесс может позже читать эти записи и выполнять действие один раз. Это делает процесс детерминированным, но ценой большей сложности, дополнительных таблиц и фоновых процессов. Это часто оверинжиниринг, если только побочный эффект не является критическим или необратимым, например, платёж.

Компромисс сводится к оценке последствий. Если последствия повторения действия существенны, явно добавьте идемпотентность. В противном случае повтор операции может быть приемлемым.

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

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

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

Не применяйте шаблон «Идемпотентный потребитель» бездумно везде. Применяйте его там, где он защищает вас от реального ущерба, где дублирование обработки приводит к финансовым последствиям или несогласованности данных.

Во всём остальном — чем проще, тем лучше.

Источник:
https://www.milanjovanovic.tech/blog/the-idempotent-consumer-pattern-in-dotnet-and-why-you-need-it
👍13
День 2494. #ЧтоНовенького
Модернизируем .NET-приложения с Copilot
Модернизация приложений — не просто поддержание актуальности версии. Старые фреймворки могут создавать риски безопасности и снижать производительность. Обновление старого приложения .NET не обязательно означает поиск неисправных сборок и непонятных ошибок. Однако для многих разработчиков простое обновление версии оборачивается часами ручных исправлений и борьбы с конфликтами зависимостей.

GitHub Copilot - помощник по модернизации, который проведёт вас через каждый этап, автоматизирует сложную работу и поможет перейти от «это может занять несколько недель» к «сделано за несколько часов».

Предварительные требования
- Visual Studio 2026 (или VS 2022 версии 17.14.17 или новее);
- Лицензия GitHub Copilot (Pro, Pro+, Business или Enterprise);
- Рабочая нагрузка .NET Desktop с включёнными компонентами: GitHub Copilot и GitHub Copilot app modernization.

Обновление приложения .NET
1. Откройте проект или решение
Начните с запуска Visual Studio и откройте свой проект или решение .NET.

2. Начните сеанс агента
- Щёлкните правой кнопкой мыши по проекту или решению в обозревателе решений и выберите Modernize (Модернизировать).
- Либо откройте чат GitHub Copilot и введите @modernize, а затем ваш запрос.

3. Выберите путь
Выберите опцию Upgrade to a newer version of .NET (Обновиться до более новой версии .NET).

4. Оценка и планирование
Copilot оценит ваш код и зависимости, затем:
- Задаст несколько вопросов о ваших целях, чтобы адаптировать план.
- Создаст план обновления в формате Markdown для прозрачности (см. картинку 1).
- Позволит просмотреть и отредактировать план перед дальнейшими действиями.
Вы можете отредактировать план, добавив контекст, изменив порядок этапов или исключив определённые проекты перед утверждением.

5. Применение изменений и устранение ошибок
После утверждения плана Copilot:
- Автоматически обновит файлы, скорректирует импорт и исправит синтаксические ошибки;
- Обработает ошибки сборки в цикле исправления и тестирования для обеспечения стабильности;
- Будет отслеживать ход выполнения в документе «Подробности обновления» для наглядности;
- Будет фиксировать каждый важный шаг в Git для лёгкого отката при необходимости;
- Если Copilot обнаружит проблему, которую не может исправить автоматически, он приостановит работу и запросит действия от вас, сохраняя контроль над процессом.

6. Проверка результатов
После завершения Copilot предоставит:
- Подробный отчёт с хешами коммитов Git для отслеживания;
- Раздел «Следующие шаги» для действий после обновления, таких как обновление конвейеров CI/CD или запуск интеграционных тестов.

Источник: https://devblogs.microsoft.com/dotnet/modernizing-dotnet-with-github-copilot-agent-mode/
👎21👍8
День 2495. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

10. Управление памятью и сборка мусора
«Объясните роль сборки мусора в .NET и расскажите, как она помогает управлять памятью? С какими потенциальные проблемами могут столкнуться разработчики при сборке мусора, и какие есть способы их решения?»

Хороший ответ
«Сборка мусора (GC) в .NET — это форма автоматического управления памятью. Сборщик мусора управляет освобождением памяти для приложений. При создании экземпляров объектов в .NET среда CLR (Common Language Runtime) выделяет для них память в управляемой куче. По мере создания новых объектов куча заполняется, и память необходимо освобождать. Сборщик мусора автоматизирует этот процесс, периодически определяя объекты, которые больше не используются приложением, и освобождая занимаемую ими память.

Сборщик мусора работает по модели поколений для повышения производительности, разделяя кучу на три поколения:
- 0 - короткоживущие объекты. GC очищает это поколение чаще всего.
- 1 - служит буфером между короткоживущими и долгоживущими объектами.
- 2 - содержит долгоживущие объекты.
Объект изначально помещается в поколение 0. Если он «выжил» при первой сборке мусора, он перемещается в поколение 1, потом – в поколение 2. Идея в том, что объекты, которые используются долгое время, скорее всего продолжат использоваться и далее, поэтому не имеет смысла часто их проверять.

После удаления неиспользуемых объектов сборщик мусора иногда также перемещает «выжившие» объекты в начало кучи. Это уменьшает фрагментированность памяти и повышает производительность приложений.

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

2. Утечки памяти
Утечки памяти могут возникать и в управляемых языках, таких как C#. Разработчики должны гарантировать, что объекты, занимающие большой объём памяти, правильно удаляются и не сохраняются случайно в глобальных или статических ссылках.
Разработчики могут влиять на производительность сборки мусора, например, используя слабые ссылки для больших объектов, к которым редко обращаются, и минимизируя использование финализаторов, которые могут задерживать сборку мусора.

Часто встречающийся некорректный ответ
«Сборка мусора в .NET автоматически очищает все неиспользуемые объекты, поэтому .NET разработчикам не нужно беспокоиться об управлении памятью. GC всё делает за нас, гарантируя, что приложение использует минимум памяти».

Этот ответ демонстрирует ошибочное представление о сборке мусора.

- Чрезмерная уверенность в GC: Хотя GC действительно помогает управлять памятью, разработчикам всё равно следует осознавать, сколько и каких объектов они создают и как управляют памятью, чтобы предотвратить такие проблемы, как утечки памяти и чрезмерное использование памяти.

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

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

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👍10
День 2496. #SystemDesign101
8 Структур Данных, Использующихся в БД

Данные могут быть индексированы в памяти или на диске. Аналогично, форматы данных различаются, например, числа, строки, географические координаты и т.д. Система может быть ориентирована на запись или чтение. Все эти факторы влияют на выбор формата индекса базы данных.

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

1. Skiplist
Вероятностная структура данных, позволяющая в среднем за O(log(n)) времени выполнять операции добавления, удаления и поиска элементов. Распространённый тип индекса в памяти. Используется в Redis.

2. Хэш-индекс
Структура данных, используемая для быстрого поиска точных совпадений в БД, основанная на хэш-таблицах. Работает с помощью хэш-функции, которая преобразует значение ключа в хэш-код (число), а затем используется для определения "бакета" (корзины), где хранится ссылка на нужную запись.

3. SS-таблица
Формат хранения данных в виде неизменяемого файла на диске, содержащего отсортированные по ключам пары "ключ-значение". Данные из временной памяти (Memtable) сбрасываются на диск в виде SS-таблицы, что делает их постоянными и отсортированными для быстрого доступа.

4. LSM-дерево
Skiplist + SSTable. Cтруктура данных, используемая в БД для эффективного хранения и обработки большого количества записей, особенно при частых вставках и удалениях. Новые данные помещаются в отсортированный буфер в оперативной памяти (Memtable), затем периодически сбрасываются на диск в виде SS-таблиц, которые затем объединяются в фоновом режиме.

5. B-дерево
Сбалансированная древовидная структура данных, которая оптимизирована для работы с большими объёмами информации, хранящейся на диске или во внешней памяти. Каждый узел B-дерева может содержать множество ключей и ссылок на множество потомков, что позволяет уменьшить высоту дерева и, как следствие, сократить количество операций чтения-записи, что критически важно для БД и файловых систем.

6. Инвертированный индекс
Структура данных, которая сопоставляет слова с документами, в которых они встречаются. Является ключевым элементом поисковых систем, так как позволяет быстро находить документы по заданному слову или фразе, перебирая списки документов, а не все документы целиком. Используется в Lucene.

7. Суффиксное дерево
Cжатое дерево, представляющее все суффиксы заданной строки. Позволяет быстро решать задачи, связанные с поиском подстрок, такие как поиск вхождений, поиск самых длинных общих подстрок и т.п.

8. R-дерево
Древовидная структура данных для индексации многомерной, в основном пространственной, информации, такой как географические координаты, прямоугольники или многоугольники. Позволяет эффективно выполнять запросы к таким данным, разбивая пространство на перекрывающиеся области с помощью ограничивающих прямоугольников, и организует объекты в узлах дерева для быстрой фильтрации и поиска.

Источник: https://blog.bytebytego.com
👍16
День 2497. #ЗаметкиНаПолях
Используем Выражения Коллекций для Пользовательских Типов

В C#12 появились выражения коллекций — новый упрощённый синтаксис для инициализации коллекций:
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];

Этот синтаксис отлично работает со встроенными типами коллекций, но как насчёт наших типов коллекций? Здесь на помощь приходит атрибут CollectionBuilderAttribute, позволяющий расширить этот современный синтаксис на пользовательские типы.

Пользовательские коллекции
Представьте, что вы создали свой тип коллекции:
public class MyCollection<T> 
{
private readonly List<T> _items;

public MyCollection(ReadOnlySpan<T> items) =>
_items = [.. items];

public IEnumerator<T> GetEnumerator()
=> _items.GetEnumerator();

// другие члены…
}

Раньше вы не могли использовать синтаксис выражений коллекций для этого типа. Вам приходилось использовать старый способ:
var myCol = 
new MyCollection<int>(new[] { 1, 2, 3, 4, 5 });

Либо (что не сильно меняет дело):
var myCol = 
new MyCollection<int>([1, 2, 3, 4, 5]);

Не слишком элегантно.

Атрибут CollectionBuilderAttribute устраняет этот разрыв, сообщая компилятору, как конструировать вашу коллекцию из выражения коллекции:
[CollectionBuilder(typeof(MyCollectionBuilder),
nameof(MyCollectionBuilder.Create))]
public class MyCollection<T>
{
//…
}

public static class MyCollectionBuilder
{
public static MyCollection<T>
Create<T>(ReadOnlySpan<T> items)
=> new([..items]);
}

Теперь мы можем использовать:
MyCollection<int> myCol = [1, 2, 3, 4, 5];

Компилятор автоматически вызовет метод Create, передав ему элементы.

Как это работает
Атрибут принимает два параметра:
1. Тип построителя – тип, содержащий фабричный метод (typeof(MyCollectionBuilder));
2. Имя метода – имя статического метода, создающего экземпляр ("Create").

Метод построителя должен:
- быть статическим;
- принимать либо ReadOnlySpan<T> (предпочтительно), либо T[];
- возвращать экземпляр типа коллекции;
- иметь параметры типа, соответствующие вашей коллекции.

Замечание: также необходимо, чтобы коллекция имела «тип итерации», то есть имела метод GetEnumerator(), возвращающий IEnumerator (или IEnumerator<T>). Можно либо реализовать интерфейс IEnumerable или IEnumerable<T>, либо просто добавить метод GetEnumerator().

Когда всё это реализовано, компилятор может сгенерировать необходимый код для построения вашей коллекции наиболее эффективным способом.

Источник: https://bartwullems.blogspot.com/2025/11/how-to-uninstall-older-net-core-versions.html
👍20
День 2498. #Architecture
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Начало

Архитектура Вертикальных Срезов (Vertical Slice Architecture) кажется очень удобной, когда вы с ней впервые сталкиваетесь. Не нужно прыгать между семью уровнями, чтобы добавить одно поле, не нужны десятки проектов в решении. Но при реализации более сложных функций начинают проявляться недостатки.

Допустим, у нас есть срезы CreateOrder, UpdateOrder и GetOrder. Внезапно возникает повторение: логика проверки адреса заказа в 3х местах. А подсчёт подытога, налогов и итога нужен как для корзины, так и для оформления заказа. Хочется создать общий проект или папку SharedServices. Это самый критический момент во внедрении VSA. Выберете неправильный вариант, и снова создадите связанность, от которой пытались избавиться. Выберите правильный - сохраните независимость, которой отличается VSA.

Чистая архитектура устанавливает строгие ограничения. Она точно определяет, где находится код:
- сущности - в домене (Domain),
- интерфейсы — в приложении (Application),
- реализации — в инфраструктуре (Infrastructure).
Это безопасно, предотвращает ошибки, но также препятствует использованию обходных путей, даже когда они уместны.

VSA устраняет ограничения. Она гласит: «Организуйте код по функциям, а не по техническим особенностям». Это обеспечивает скорость и гибкость, но перекладывает бремя дисциплины на вас. Что делать?

Ловушка: «Общий» мусорный ящик
Путь наименьшего сопротивления — создать проект (или папку) с именем Shared, Common или Utils. Это почти всегда ошибка.

Представьте проект Common.Services с классом OrderCalculationService:
- метод для итогов корзины (используется Cart),
- для истории доходов (используется Reporting),
- метод для форматирования счетов (используется Invoices).
3 несвязанные задачи. 3 разные причины изменений. Класс, объединяющий всё.

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

Схема принятия решений
В потенциальной ситуации совместного использования кода нужно задать 3 вопроса:

1. Это инфраструктурный или доменный код?
Инфраструктура (контексты БД, ведение журнала, HTTP-клиенты) почти всегда используются из нескольких мест. Концепции домена требуют более тщательного изучения (об этом позже).

2. Насколько стабильна концепция?
Если меняется раз в год – можно кидать в общий код. Если при каждом запросе на новую функцию, оставьте её локальной.

3. Нарушается ли «правило трёх»?
Дублирование кода 1 раз допустимо. Создание 3х копий должно насторожить. Не абстрагируйтесь, пока не достигнете трёх.

Мы решаем эту проблему рефакторингом кода. Далее рассмотрим несколько примеров.

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

Источник:
https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
👍12
День 2499. #Architecture
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Продолжение

Начало

Три уровня общего кода
Вместо бинарного разделения «общий/локальный код» мыслите тремя уровнями.

Уровень 1. Техническая инфраструктура (спокойно используйте общий код)
Чистая инфраструктура, одинаково влияющая на все срезы: логеры, фабрики подключений к БД, промежуточное ПО аутентификации, шаблон Result, конвейеры валидации и т.п. Централизуйте всё это в проекте Shared.Kernel или Infrastructure. Обратите внимание, что это также может быть папка внутри проекта. Такой код редко меняется в связи с бизнес-требованиями.
// Техническое ядро
public readonly record struct Result
{
public bool IsSuccess { get; }
public string Error { get; }

private Result(bool isSuccess, string error)
{
IsSuccess = isSuccess;
Error = error;
}

public static Result Success()
=> new(true, string.Empty);
public static Result Failure(string error)
=> new(false, error);
}


Уровень 2. Концепции домена (используйте общий код)
Вместо того, чтобы разбрасывать бизнес-правила по срезам, передавайте их в сущности и объекты-значения:
// Сущность с бизнес-логикой 
public class Order
{
public Guid Id { get; set; }
public OrderStatus Status { get; set; }
public List<OrderLine> Lines { get; set; }

public bool CanBeCancelled() =>
Status == OrderStatus.Pending;

public Result Cancel()
{
if (!CanBeCancelled())
return Result.Failure("Нельзя отменить подтверждённый заказ.");

Status = OrderStatus.Cancelled;
return Result.Success();
}
}

CancelOrder, GetOrder и UpdateOrder все используют одни бизнес-правила. Логика живёт в одном месте. Т.е. разные вертикальные срезы могут использовать одну и ту же модель домена.

Уровень 3: Логика, специфичная для конкретного объекта (сохраняйте её локально)
Логика, общая для связанных срезов, таких как CreateOrder и UpdateOrder, не обязательно должна быть глобальной. Создайте общую папку (из каждого правила есть исключения) внутри функции:
📂 Features
📂 Orders
📂 CreateOrder
📂 UpdateOrder
📂 GetOrder
📂 Shared
📄 OrderValidator.cs
📄 OrderPricingService.cs

Это также имеет скрытое преимущество. Если вы удалите функцию Orders, общая логика будет удалена вместе с ней. Никакого зомби-кода.

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

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

Источник:
https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
👍10
День 2500. #Architecture
Архитектура Вертикальных Срезов. Где Живёт Общая Логика? Окончание

Начало
Продолжение

Совместное использование между функциями
Что насчёт совместного использования кода между несвязанными функциями в VSA? CreateOrder должен проверять наличие клиента. GenerateInvoice должен рассчитывать налог. И Orders и Customers должны форматировать уведомления. Это не совсем вписывается в папку Shared определённой функции. Куда деть эту логику?

1. Действительно ли нужен общий код?
Большинство случаев «совместного использования» между функциями — это просто замаскированный доступ к данным. Если CreateOrder нужны данные о клиентах, он напрямую обращается к БД. Он не обращается к функции Customers. Каждый срез владеет своим доступом к данным. Сущность Customer является общей (находится в домене), но Orders и Customers не имеют общего сервиса.

2. Когда действительно нужна общая логика
- Логика домена (бизнес-правила, калькуляции) → Domain/Services
- Инфраструктура (внешние API, форматирование) → Infrastructure/Services
// Domain/Services/TaxCalculator.cs
public class TaxCalculator
{
public decimal CalculateTax(
Address address, decimal subtotal)
{
var rate = GetTaxRate(address.State, address.Country);
return subtotal * rate;
}
}

CreateOrder и GenerateInvoice могут использовать этот код без привязки друг к другу.

Прежде чем создавать какой-либо кросс-функциональный сервис, спросите себя: может ли эта логика размещаться в доменной сущности? Большая часть «общей бизнес-логики» на самом деле представляет собой доступ к данным, доменную логику, принадлежащую сущности, или преждевременную абстракцию.

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

3. Когда дублирование — правильный выбор
Иногда «общий» код только кажется общим:
// Features/Orders/GetOrder
public record GetOrderResponse(Guid Id, decimal Total, string Status);

// Features/Orders/CreateOrder
public record CreateOrderResponse(Guid Id, decimal Total, string Status);

Они идентичны. Возникает соблазн создать SharedOrderDto. Не поддавайтесь ему.

На следующей неделе в GetOrder понадобится URL трекинга. Но CreateOrder выполняется до отправки, поэтому не имеет URL. Если бы используете общий класс DTO, у вас появится свойство, допускающее NULL, которое в половине случаев остаётся пустым, что сбивает с толку. Дублирование дешевле, чем неправильная абстракция.

Структура Проекта
Вот как может выглядеть структура проекта VSA:
📂 src
📂 Features
│ ├📂 Orders
│ │ ├📂 CreateOrder
│ │ ├📂 UpdateOrder
│ │ └📂 Shared
│ ├📂 Customers
│ │ ├📂 GetCustomer
│ │ └📂 Shared
│ └📂 Invoices
│ └📂 GenerateInvoice
📂 Domain
│ ├📂 Entities
│ ├📂 ValueObjects
│ └📂 Services
📂 Infrastructure
│ ├📂 Persistence
│ └📂 Services
📂 Shared
📂 Behaviors

- Features — Изолированные срезы. Каждый со своими моделями запроса/ответа.
- Features/[Name]/Shared — Локальный общий код между связанными срезами.
- Domain — Сущности, объекты-значения и доменные сервисы. Общая бизнес-логика живёт здесь.
- Infrastructure — Технические вопросы.
- Shared — Только общее поведение.

Правила
1. Функции владеют своими моделями запросов/ответов. Исключений нет.
2. Перемещайте бизнес-логику в домен. Сущности и объекты-значения — лучшее место для совместного использования бизнес-правил.
3. Сохраняйте совместное использование семейств функций локальным. Если это нужно только срезам Order, храните его в Features/Orders/Shared (не стесняйтесь найти более подходящее название, чем Shared).
4. Инфраструктура по умолчанию является общей. Контексты БД, HTTP-клиенты, ведение журнала. Это технические вопросы.
5. Применяйте правило трёх. Не извлекайте данные, пока не найдёте 3 реальных использования идентичной, стабильной логики.

Источник: https://www.milanjovanovic.tech/blog/vertical-slice-architecture-where-does-the-shared-logic-live
👍17
День 2501.
Сегодня порекомендую видео от моего любимого докладчика Дилана Бити. Недавно на конференции NDC в Копенгагене он выступал с докладом “Algorithms Demystified”.

Бывало ли у вас, что вы застревали на какой-то проблеме с кодом? Возможно, реализовывали какую-то функцию в одном из своих проектов, или решали задачи на LeetCode или Advent of Code, и застряли. Вы не можете понять, как получить нужный результат. Поэтому спрашиваете у коллег, на Stack Overflow или Reddit, и получаете ответ вроде: «Да тут просто надо использовать алгоритм Дейкстры»… и ваш мозг зависает. Использовать что? Вы гуглите и обнаруживаете, что это «поиск кратчайших путей между узлами во взвешенном графе», и теперь нужно узнать, что такое «узел» и что такое «взвешенный граф»… а затем понять, как превратить всё это в работающий код.

Алгоритмы — ключ ко всем видам функций и систем, от сетей до автокоррекции, и понимание того, как они работают, поможет вам создавать более качественное программное обеспечение, исправлять едва заметные ошибки и решать задачи на Advent of Code. В этом докладе Дилан расскажет о некоторых из его любимых алгоритмов, объяснит их важность и поможет вам понять, что они делают и как.
51👍13
День 2502. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.

11. Различия между современным .NET и .NET Framework
«Объясните ключевые различия между современным .NET и .NET Framework? Также обсудите ситуации, в которых один вариант может быть более подходящим, чем другой».

Хороший ответ
Современный .NET и .NET Framework — это фреймворки, разработанные Microsoft, но они предназначены для разных потребностей и сценариев.

- Поддержка кроссплатформенности: современный .NET кроссплатформенный, поддерживает Windows, Linux и macOS, что делает его подходящим для приложений, требующих широкого охвата в различных операционных системах. .NET Framework же ограничен Windows.

- Производительность и масштабируемость: современный .NET оптимизирован для производительности и масштабируемости. Он включает в себя такие усовершенствования, как веб-сервер Kestrel и компилятор RyuJIT, которые эффективнее и быстрее аналогов из .NET Framework. Это делает современный .NET идеальным для создания высокопроизводительных и масштабируемых веб-приложений.

- Архитектура микросервисов: современный .NET разработан для поддержки архитектуры микросервисов, позволяя разработчикам создавать и развертывать независимо обновляемые и масштабируемые сервисы. Он легковесен и имеет встроенную поддержку технологий контейнеризации, таких как Docker.

- Совместимость: .NET Framework поддерживает более широкий спектр старых API .NET и сторонние библиотеки, которые могут быть недоступны в современной .NET. Этот подход часто выбирают для устаревших приложений, использующих эти API, и для проектов, где совместимость со старыми технологиями .NET критически важна.

Инструменты и обновления: современный .NET выигрывает от параллельного управления версиями, которое позволяет нескольким версиям среды выполнения существовать на одной машине. Это особенно полезно для тестирования новых функций, не затрагивая существующие приложения. С другой стороны, .NET Framework не поддерживает эту функцию и требует обновления единственного экземпляра на хосте.

Подводя итог, можно выделить следующие сценарии, в которых один вариант может быть более подходящим:
- Современный .NET подходит для новых корпоративных приложений, особенно тех, которые требуют кроссплатформенной функциональности, архитектуры микросервисов или требуют работы в контейнерных средах.
- .NET Framework подойдёт для поддержки существующих приложений, использующих старые библиотеки или специфичные для Windows API, которые не поддерживаются современным .NET.

Часто даваемый неудачный ответ
«NET Core — это просто более новая версия .NET Framework, поэтому всегда лучше использовать .NET Core для всех проектов, поскольку он новее и в конечном итоге заменит .NET Framework.»

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

- Непонимание области применения и вариантов использования: Предложение использовать .NET Core или современный .NET для всех проектов игнорирует сценарии, в которых .NET Framework может по-прежнему быть необходим из-за совместимости со старыми технологиями и широкого использования в корпоративном секторе.

- Игнорирование проблем совместимости: Утверждение, что современный .NET заменит .NET Framework, не учитывает тот факт, что некоторые приложения зависят от функций и библиотек, доступных только в .NET Framework.

- Упрощение выбора технологий: Выбор технологий должен основываться на конкретных требованиях, возможностях и стратегических целях, а не просто Выбор чего-то, что новее.

Эта распространённая ошибка происходит из-за отсутствия глубокого понимания экосистем обоих фреймворков, что приводит к неверной оценке их применимости.

Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👎11👍6
День 2503. #SystemDesign101
8 Ключевых Концепций DDD


1. Предметно-ориентированное проектирование (Domain Driven Design)
Предполагает разработку программного обеспечения посредством моделирования предметной области. Единый язык — одна из ключевых концепций DDD. Модель предметной области — связующее звено между бизнес-доменом и программным обеспечением.

2. Бизнес-сущности
Использование моделей может помочь в выражении бизнес-концепций и знаний, а также в руководстве дальнейшей разработкой программного обеспечения, такого как базы данных, API и т. д.

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

4. Агрегация
Агрегат — это кластер связанных объектов (сущностей и объектов-значений), которые рассматриваются как единое целое при изменении данных. Обращение ко всем объектам агрегата должно осуществляться только через главный объект – корень агрегата.

5. Сущности или объекты-значения
Помимо корней агрегатов и сущностей, существуют некоторые модели, которые выглядят как одноразовые: у них нет собственного идентификатора, который бы их идентифицировал, но они являются частью некой сущности, представляющей собой набор из нескольких полей.

6. Манипуляции с моделями
В DDD для манипуляций с этими моделями используется ряд объектов, которые действуют как «операторы»:
- фабрики – для создания объектов,
- сервисы – для управления моделями, исполняя бизнес-логику,
- репозитории – для сохранения и извлечения моделей из хранилища.

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

8. Построение модели предметной области
Набор методов для извлечения моделей предметной области из бизнес-знаний.

Источник: https://bytebytego.com/guides/8-key-concepts-in-ddd/
👎8👍6
День 2504. #ЗаметкиНаПолях
Использование Сортируемых UUID/GUID в Entity Framework

В .NET 9 появились методы Guid.CreateVersion7() и Guid.CreateVersion7(DateTimeOffset) для создания сортируемых UUID/GUID по времени их создания. Это может быть особенно полезно в базах данных, где требуется поддерживать хронологический порядок записей (плюс некоторые преимущества в плане производительности). В настоящее время в Entity Framework нет встроенного способа настроить использование этих методов при генерации новых GUID для первичных ключей. Но мы можем сделать это самостоятельно.

Генератор значений
Для начала нам нужно создать пользовательский генератор значений, который будет использовать Guid.CreateVersion7() для генерации новых GUID:
public class UUIDv7Generator 
: ValueGenerator<Guid>
{
public override bool
GeneratesTemporaryValues => false;

public override Guid Next(EntityEntry entry)
=> Guid.CreateVersion7();
}


Использование:
public class MyDbContext : DbContext
{
protected override void
OnModelCreating(ModelBuilder mb)
{
mb.Entity<MyEntity>()
.Property(e => e.Id)
.HasValueGenerator<UUIDv7Generator>()
.ValueGeneratedOnAdd();
}
}


И вуаля! Теперь, когда для объектов MyEntity свойство Id будет генерироваться в виде UUID версии 7. Если вы хотите, чтобы появился более удобный способ генерации таких значений в Entity Framework, можете проголосовать за этот тикет.

Источник: https://steven-giesel.com/blogPost/d6150b89-a3ef-407e-add2-7afa4a2a8729/using-sortable-uuid-guids-in-entity-framework
👍27
День 2505. #ЗаметкиНаПолях
Кэширование в Redis с Двойным Ключом. Начало
Кэширование в Redis — это просто… пока вы не осознаете, что вашей сущности нужны два разных ключа поиска: один внутренний и один внешний. Тут всё становится сложнее: если не быть внимательным, приложение может стать жертвой устаревших данных, пропущенных инвалидаций и несогласованного состояния между сервисами.

Проблема
Представьте, что вы разрабатываете типичное приложение. У каждого пользователя есть:
- UserId (GUID, внутренний, неизменяемый)
- Email (используется для входа, внешний, изменяемый)
Теперь рассмотрим, как система взаимодействует с этим профилем пользователя.

Вариант 1. Аутентификация (Email → User)
При входе в систему служба идентификации должна:
1. Найти пользователя по email;
2. Загрузить его учётные данные;
3. Загрузить его профиль (роли, настройки и т.п.)
Для этого требуется быстрый поиск по email. Выполнение этого SQL-запроса в часы пиковых нагрузок может замедлить систему. Redis решает эту проблему.

Но остальная бизнес-логика ведёт себя по-другому…

Вариант 2. Внутренние микросервисы (UserId → Профиль)
Биллинг, уведомления, аналитика и журналы аудита — все они идентифицируют пользователей по внутреннему UserId. Поэтому они ожидают быстрого поиска по UserId.

Если вы кэшируете только по одному ключу:
- UserId - трафик аутентификаций приводит к задержкам,
- Email - внутренние сервисы постоянно используют БД.

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

Но есть и более серьёзная проблема…
Наивный разработчик может сказать: «Просто храните полный JSON объекта под обоими ключами!»

Это сработает, на время. Но,
- если пользователь сменит email, нужно удалить запись со старым ключом;
- приходится удалять два элемента каждый раз при изменении пользовательских данных;
- сбои в работе сети между двумя вызовами StringSetAsync = повреждение кэша;
- вы тратите память, храня дублирующиеся JSON-объекты.

Поэтому профессиональные системы используют другой подход.

Единый источник данных + ключ индекса
Храним полный профиль пользователя ОДИН РАЗ:
user : data : {userId} → JSON


Храним индекс Email → UserId:
user : email : {email} → userId


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

Далее посмотрим, как это реализовать в .NET.

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

Источник:
https://thecodeman.net/posts/dual-key-redis-caching-in-dotnet
1👍16
День 2506. #ЗаметкиНаПолях
Кэширование в Redis с Двойным Ключом. Окончание

Начало

Реализация Двойного Ключа в .NET
1. DTO + Хелпер для ключей Redis
public class UserDto
{
public Guid UserId { get; set; }
public string Email { get; set; }
public string DisplayName { get; set; }
public string TimeZone { get; set; }
}

public static class UserKeys
{
public static string Data(Guid id)
=> $"user:data:{id}";

public static string Email(string email)
=> $"user:email:{email.ToLowerInvariant()}";
}


2. Кэширование пользователя (атомарная запись)
public async Task CacheUserAsync(UserDto user)
{
var dataKey = UserKeys.Data(user.UserId);
var emailKey = UserKeys.Email(user.Email);
var json = JsonSerializer.Serialize(user);
var tran = _db.CreateTransaction();

tran.StringSetAsync(dataKey, json, TimeSpan.FromMinutes(10));
tran.StringSetAsync(emailKey, user.UserId.ToString(), TimeSpan.FromMinutes(10));

await tran.ExecuteAsync();
}


- оба ключа обновляются совместно;
- исключён риск частичных записей;
- JSON хранится 1 раз.

3. Поиск по UserId
public async Task<UserDto?> 
GetByIdAsync(Guid userId)
{
var json = await _db.StringGetAsync(
UserKeys.Data(userId));

return json.IsNullOrEmpty
? null
: JsonSerializer.Deserialize<UserDto>(json);
}


4. Поиск по Email
public async Task<UserDto?>
GetByEmailAsync(string email)
{
var id = await _db.StringGetAsync(UserKeys.Email(email));
if (string.IsNullOrEmpty(id)) return null;

var userId = Guid.Parse(id);
return await GetByIdAsync(userId);
}


5. Безопасная обработка изменений email
public async Task UpdateEmailAsync(
Guid userId,
string oldEmail,
string newEmail)
{
var oldKey = UserKeys.Email(oldEmail);
var newKey = UserKeys.Email(newEmail);

var tran = _db.CreateTransaction();

tran.KeyDeleteAsync(oldKey);
tran.StringSetAsync(newKey, userId.ToString());

await tran.ExecuteAsync();
}

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

Если вы не будете использовать кэширование с двойным ключом, то рано или поздно, вы столкнетесь с…
- устаревшими данными в кэше;
- неработающим входом в систему (изменён email, но кэш не обновился);
- внутренними микросервисами, возвращающими устаревшие значения;
- «фантомными пользователями» в логах;
- и т.п.
Большинство этих ошибок никогда не проявятся в процессе разработки — только в рабочей среде под реальной нагрузкой.

Шаблон кэширования с двойным ключом универсален для современных систем:
1. Электронная коммерция
- ProductId → данные
- Артикул → ProductId

2. CMS
- ContentId → данные
- Slug → ContentId

3. IoT
- DeviceId → данные
- MAC-адрес/Серийный номер → DeviceId

Итого
Кэширование Redis с двойным ключом — не оптимизация, а фундаментальная архитектура для современных систем .NET, использующих Redis. Его следует использовать, когда:
- сущность имеет несколько идентификаторов;
- один или несколько из этих идентификаторов изменяемы;
- необходим быстрый поиск из разных контекстов;
- нужно избежать дублирования JSON в Redis;
- важна согласованность кэша под нагрузкой.

Правильный шаблон:
- один канонический ключ данных;
- несколько легковесных ключей индекса;
- атомарные обновления для обеспечения согласованности.
Так вы избежите почти всех проблем с несогласованностью кэша еще до их появления.

Источник: https://thecodeman.net/posts/dual-key-redis-caching-in-dotnet
👍8