C# Portal | Программирование – Telegram
C# Portal | Программирование
14.9K subscribers
965 photos
117 videos
24 files
807 links
Присоединяйтесь к нашему каналу и погрузитесь в мир для C#-разработчика

Связь: @devmangx

РКН: https://clck.ru/3FocB6
Download Telegram
Шокирующие бенчмарки производительности итераторов в C# / .NET.

Что лучше использовать в API для получения данных — итератор или материализованную коллекцию? В этом видео разбирают их реальные характеристики производительности, сравнивая итераторы в C# / .NET с обычными коллекциями на бенчмарках.

После этого видео стоит посмотреть продолжение:
https://youtube.com/watch?v=0s_VMhZSOwQ

Также можно идти параллельно по статье в блоге:
https://devleader.ca/2023/03/17/shocking-results-from-collection-and-iterator-benchmarks/

Исходники из видео здесь:
https://github.com/ncosentino/DevLeader/tree/master/AllAboutEnumerables/AllAboutEnumerables.BasicIteratorBenchmarks

Если хочется глубже разобраться в enumerable, итераторах и коллекциях, смотри плейлист:
https://youtube.com/watch?v=RR7Cq0iwNYo&list=PLzATctVhnsgjE3qOsbkPaC1NxXD605wOC

Само видео:
https://youtube.com/watch?v=G2-d7kZFlRA

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
Давка кэша в C#: почему ConcurrentDictionary и MemoryCache не спасают

В статье разбирается проблема cache stampede и то, как она приводит к многократным тяжёлым запросам. Показаны бенчмарки для IO-bound и CPU-bound сценариев, есть сравнение с HybridCache и разбор подходов к защите кэша.

Показано, что GetOrAdd и GetOrCreateAsync не гарантируют единичный вызов тяжёлой операции. HybridCache умеет защищать только внутри одного процесса, но не между репликами. В конце — практические рекомендации, как выбирать решения и обходить проблему.

Предлагается добавлять случайный джиттер к TTL (в FusionCache это уже есть) и использовать паттерн Single Flight на уровне L2, чтобы обновление тяжёлых данных выполняла только одна реплика. Готовых библиотек под это автор не нашёл.

Читать подробнее: https://habr.com/ru/articles/977498/

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
1👍1
ToArrayAsync или ToListAsync в Entity Framework?

Очевидно, каждый из этих методов стоит использовать в своей ситуации. Но если разницы нет, что выбрать? Короткий ответ: ToListAsync.

Посмотрим на реализацию. ToListAsync выглядит так:

public static async Task<List<TSource>>
ToListAsync<TSource>(
this IQueryable<TSource> source,
CancellationToken cancellationToken = default)
{
var list = new List<TSource>();
await foreach (var element in
source
.AsAsyncEnumerable()
.WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
list.Add(element);
}

return list;
}


Источник

А вот как реализован ToArrayAsync:

public static async Task<TSource[]>
ToArrayAsync<TSource>(
this IQueryable<TSource> source,
CancellationToken cancellationToken = default)
=> (
await source
.ToListAsync(cancellationToken)
.ConfigureAwait(false))
.ToArray();


Источник

Итого: ToArrayAsync сначала вызывает ToListAsync, а потом превращает полученный список в массив через ToArray(). В результате появляются лишние накладные расходы: создание списка, его наполнение, затем создание массива и копирование элементов.

Это не относится к ToHashSetAsync — он реализован по той же схеме, что и ToListAsync, без промежуточного списка.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍144
Как включать фичи без редеплоя приложения?

Используй feature flags

Feature flags в dotnet позволяют на лету включать или выключать функциональность во время работы приложения. Так проще контролировать поведение без нового релиза.

Это удобно для постепенных раскаток, A/B тестов и безопасного выкатывания новых фич.

Ключевые идеи:

• Feature Flags: переключатели, которые определяют, включена фича или нет.
• Feature Management: встроенная поддержка в .NET 8 для управления флагами через API feature management.
• Gradual Rollout: можно открыть фичу части пользователей и только потом включить всем.
• Configuration Sources: флаги можно хранить в разных источниках, например appsettings.json, Azure App Configuration или во внешних сервисах.
• Feature Filters: условия и правила, когда фича должна включаться (роль пользователя, окружение, кастомная логика и т.д.).

Пример:

Есть новая фича, и ты хочешь проверить, как пользователи на неё отреагируют.
Feature flags позволяют показать её только определённому проценту аудитории, например 10%.

Допустим, за день на сайт заходит 1000 человек. Тогда 100 из них увидят новую фичу.
Так можно аккуратно посмотреть реакцию на изменения и не рисковать всем трафиком сразу.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍93
This media is not supported in your browser
VIEW IN TELEGRAM
Gemini Code Wiki

Бесплатный инструмент, который генерирует интерактивную документацию по любому GitHub-репозиторию 🤯

Что он умеет:
→ можно общаться с Gemini и разбираться в любом репо
→ визуализирует структуру кода
→ генерирует документацию из публичных репозиториев

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍64😁2
Используем выражения коллекций для пользовательских типов

В 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


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().

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

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7👍42🍌1
PostgreSQL позволяет клонировать базу на 6 ГБ за 212 миллисекунд вместо 67 секунд. Вот как это работает.

Клонирование баз данных полезно в нескольких сценариях:

* тестирование миграций без трогания продакшн-данных
* поднятие свежих копий под каждый прогон тестов
* сброс sandbox-окружения между сессиями
* воспроизводимые снапшоты для отладки

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

В PostgreSQL система шаблонов была всегда. Каждый CREATE DATABASE тихо клонирует template1 под капотом, и вместо template1 можно использовать любую базу.

В версии 15 появился параметр STRATEGY, и по умолчанию включили WAL_LOG — поблочное копирование через журнал предзаписи (WAL). I/O становится ровнее, но для больших баз это медленнее.

В PostgreSQL 18 появилась опция file_copy_method = clone. На современных файловых системах вроде XFS, ZFS или APFS она использует операцию FICLONE. Вместо копирования байтов файловая система создаёт новые метаданные, которые указывают на те же физические блоки. Обе базы используют одно и то же хранилище, пока вы ничего не меняете.

Всю магию здесь делает файловая система, создавая copy-on-write (CoW) клон файлов.

Когда вы обновляете строку, файловая система запускает copy-on-write только для затронутых страниц. Всё остальное остаётся общим. Клон на 6 ГБ изначально не занимает дополнительного места и растёт только по мере расхождения данных.

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

Довольно круто.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
10🔥7🎉3👍1🍌1
Пример кода с FormattableString в C#

- Создание из списка с цветом для вывода
- Достаём нужные свойства через extension-методы
- Фильтрация в простом варианте

Читать подробнее

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
👎4👍3🍌1
Вы всё ещё вызываете new Random() по каждому поводу? Пора перестать.

В сети часто повторяют: new Random() — это плохо.
Мол, он берёт сид из системных часов, и если создать несколько экземпляров подряд, то значения будут одинаковыми. Типа такой код:

for (var i = 0; i < 5; i++)
{
var random = new Random();
Console.WriteLine(random.Next(1, 100));
}


выведет:

42 42 42 42 42


А вот этот вариант:

for (var i = 0; i < 5; i++)
Console.WriteLine(Random.Shared.Next(1, 100));


даст что-то вроде:

17 82 19 23 5


Откуда ноги растут?
В старом .NET Framework new Random() реально сидировался по DateTime.Now.Ticks, которые обновлялись примерно раз в 16 мс. Если создавать экземпляры чаще, сид совпадал, и результаты были одинаковыми. В .NET Core и дальше это уже не так. Можно прогнать код и убедиться.

А как насчёт потокобезопасности?

Наивный вариант, когда делают один общий генератор:

Random rng = new();
Parallel.For(0, 10, x =>
{
var value = rng.Next();
Console.WriteLine(value);
});


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

Random при гонках в потоках часто возвращает 0, так что можно посмотреть масштаб проблемы:

Random rng = new();
Parallel.For(0, 10, x =>
{
var nums = new int[10_000];
for (int i = 0; i < nums.Length; ++i)
nums[i] = rng.Next();

Console.WriteLine($"{nums.Count(x => x == 0)} нулей");
});


В .NET 5 этот пример легко даёт 9000+ нулей, то есть почти всё упирается в проблемы потоков.
В .NET 8+ ситуация сильно лучше, но если задать сид вручную:

Random rng = new(123);


то проблемы возвращаются.

Что использовать?

Random.Shared создаёт свой экземпляр на поток (через [ThreadStatic]), поэтому для большинства кейсов на современных .NET это нормальный выбор. Если не нужен строго заданный сид — берите Random.Shared.
new Random() на новых версиях тоже обычно ок, но Shared безопаснее в многопоточке.

Про .NET Framework чуть позже.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
16
Проблема с Random в .NET Framework

У Random в .NET Framework по сути две основные болячки:

При new Random() в .NET Framework сид берется от системных часов.

У системных часов ограниченная точность (разрешение).

Из-за этого при быстром, плотном создании new Random() можно легко получить два (или больше) инстанса Random с одинаковым seed. А значит, они выдадут одну и ту же последовательность чисел:

// <TargetFramework>net48</TargetFramework>
Parallel.For(0, 10, x =>
{
Random rnd = new();
var value = rnd.Next();
Console.WriteLine(value);
});


В результате часть чисел (а иногда и вообще все) могут совпасть, что нам обычно вообще не нужно.

Решение

Идея простая: держим один общий Random, который используется только чтобы раздавать seed’ы для остальных Random. Раз сиды разные, то и последовательности у локальных Random не будут коррелировать, и проблема с одинаковыми значениями уйдет.

В .NET Core / .NET это как раз поведение “из коробки”, поэтому баг проявляется именно в .NET Framework.

Ниже пример обертки ThreadSafeRandom. Она использует [ThreadStatic] и аккуратно обходит проблему инициализации в .NET Framework:

internal static class ThreadSafeRandom
{
[ThreadStatic]
private static Random? _local;
private static readonly
Random Global = new Random();

private static Random Instance
{
get
{
if (_local is null)
{
int seed;
// избегаем конкурентного доступа
lock (Global)
{
seed = Global.Next();
}

_local = new Random(seed);
}
return _local;
}
}

public static int Next() => Instance.Next();
}


Эту потокобезопасную реализацию можно юзать на любых версиях фреймворка:

Parallel.For(0, 10, x =>
{
var value = ThreadSafeRandom.Next();
Console.WriteLine(value);
});


Если вы на .NET 6+, лучше использовать встроенный Random.Shared. Для более ранних версий подойдет ThreadSafeRandom выше. А если таргетитесь и в .NET 6+, и в .NET Framework, можно развести реализации через директивы #if под разные target framework.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
9👍2
Оптимизация памяти в C# (и немного в Unity): эффективные методы и стратегии

Язык программирования C#, несмотря на то, что обеспечивает автоматическое управление памятью с помощью механизма сборки мусора (GC), требует от разработчиков специальных знаний и навыков для оптимизации работы с памятью

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

Читать статью...

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8👎7
Библиотеки, которые реально используют в Microsoft.

Надоело, что на каждом углу советуют AutoMapper и Polly? Вот инструменты, которые инженеры Microsoft юзают в продакшене. Они прошли боевые условия, экономят память и закрывают задачи, которые масштабируются до уровня Bing и Azure.

1. Microsoft.IO.RecyclableMemoryStream

Проблема:

Когда в API гоняется большой трафик или крутятся экспортные задания, обычный new MemoryStream() вызывает фрагментацию LOH (Large Object Heap) и частые остановки GC. В проде типа Bing это легко выливается в
OutOfMemoryException.

Решение:

RecyclableMemoryStream:

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

Где уместно:

ASP.NET API, которые возвращают PDF, изображения, Excel,

- фоновые задачи, работающие с файлами,
- бэкенды под gRPC и SignalR.

До:

using var ms = new MemoryStream();


После:

var mgr = new RecyclableMemoryStreamManager();
using var ms = mgr.GetStream();


Бенч:

Method           | Memory | Gen 0 | Gen 2
---------------- | -------| ----- | -----
MemoryStream | 85 KB | 1 | 1
RecyclableMemory | 2 KB | 0 | 0


2. LoggerMessage (генератор кода)

Проблема:
В сервисах с высоким трафиком даже такие безобидные логи:

_logger.LogInformation($"Обработка заказа {orderId}");


создают проблемы:

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

На масштабе Azure это превращается в задержки и прожорливость по памяти.

Решение:
LoggerMessage из .NET 8 генерирует методы на этапе компиляции:

- без интерполяции и упаковки типов,
- со структурированным форматированием через Span<T>,
- с IL без лишних аллокаций,

в 5–10 раз быстрее обычного ILogger.LogInformation(...).

Где уместно:

- нагруженные веб-API,
- воркфлоу/ивент-хендлеры, где логов много,
- телеметрия и критичные hot-path’ы.

До (аллокации):

_logger.LogInformation($"Обработка запроса {orderId}");


После (генерация):

[LoggerMessage(EventId = 100, Level = LogLevel.Information, Message = "Обработка запроса {OrderId}")]
partial void LogProcessingOrder(int orderId);

// вызов
LogProcessingOrder(orderId);


Можно вынести такие методы в статические хелперы и шарить между сервисами.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥226👍3👎1🤣1
C# Portal | Программирование
Библиотеки, которые реально используют в Microsoft. Надоело, что на каждом углу советуют AutoMapper и Polly? Вот инструменты, которые инженеры Microsoft юзают в продакшене. Они прошли боевые условия, экономят память и закрывают задачи, которые масштабируются…
Ещё стоит упомянуть:

3. Microsoft.SemanticKernel

Проблема: Обычных запросов и ответов уже не хватает для продакшн-приложений на ИИ. Нужны память между диалогами, разбиение целей на шаги и доступ к API вроде Microsoft 365 Copilot. Реализовать всё это вручную в .NET — тяжело и долго.

Решение:

SemanticKernel — официальный SDK от Microsoft для сборки похожих на агентов ИИ-процессов. Он даёт:

* Вызов инструментов и плагинов (C# или OpenAPI);
* Оркестрацию цепочек запросов;
* Долговременную память между вызовами;
* Планировщик для разбиения задач на подзадачи.

Когда стоит брать:

* Корпоративные ИИ-ассистенты (CRM, генерация отчётов и т.п.);
* ИИ-процессы, которые ходят во внутренние API/функции;
* Многошаговые чат-боты с планированием и памятью.

До (одиночный запрос):

var result = await openAiClient
.GetCompletionAsync("Какая погода в Москве?");


После (оркестрация через SemanticKernel):

var kernel = Kernel.CreateBuilder().Build();
var prompt = kernel
.CreateFunctionFromPrompt("Какая погода в Москве?");
var result = await prompt.InvokeAsync();


Можно регистрировать плагины, которые вызывают API, делают авторизацию, исполняют SQL или дергают другие сервисы — и всё из .NET. Документация

4. System.Threading.Channels + IAsyncEnumerable

Проблема: BlockingCollection, ConcurrentQueue и кастомные очереди в паттерне «производитель/потребитель» часто упираются в блокировки, нестабильность и проблемы под нагрузкой. Особенно в телеметрии и потоковых пайплайнах.

Решение:

System.Threading.Channels даёт:

* Быстрый обмен сообщениями внутри процесса;
* Контроль пропускной способности через ограниченные каналы;
* Простую связку с IAsyncEnumerable<T>;
* Асинхронный стриминг без лишних выделений памяти.

Изначально жило внутри SignalR и gRPC, сейчас это нормальный способ собирать отказоустойчивые асинхронные конвейеры в .NET.

Когда использовать:

* Сбор телеметрии и обработка метрик;
* Асинхронные пайплайны логов/событий;
* Очередеподобное поведение без внешней брокерской инфраструктуры;
* Изоляция перегородками (Bulkhead) в фоновых сервисах.

До (BlockingCollection или Queue<T>):

var queue = new Queue<int>();
lock (queue) { queue.Enqueue(42); }
int item;
lock (queue) { item = queue.Dequeue(); }


После (каналы и async-потоки):

var ch = Channel.CreateUnbounded<int>();
await ch.Writer.WriteAsync(42);

await foreach (var item in ch.Reader.ReadAllAsync())
Console.WriteLine(item);


Отлично работает из коробки с await foreach и подходит для фоновых задач с I/O, стриминговых API и повторяющихся пайплайнов.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
7👍3
Может кому понадобится: проект генерирует архитектурные диаграммы из кода.

https://github.com/likec4/likec4/ 🤨

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥10🤣21
Как компилятор выводит тип из default?

default в C# штука мощная. Для ссылочных типов он даёт null, для значимых — нулевое значение. Забавно, что default(T) и new T() для структур могут вести себя по-разному. Раньше в C# нужно было писать default(T), а сейчас, если тип очевиден для компилятора, можно оставить просто default. Но как именно компилятор понимает, что за тип там должен быть? И всегда ли можно доверять этому выводу?

Разберём простые варианты:

// Тип тянется из левой части
int foo = default;

// Тип тянется из параметра
Foo(default);
void Foo(int bar) => throw null;


Тут всё понятно. Но что происходит в switch-выражении?

var sample = new byte[0];
ReadOnlyMemory<byte> foo = sample switch
{
byte[] value => value,
_ => default
};


Кажется логичным ожидать, что default станет default(ReadOnlyMemory<byte>), ведь слева стоит такой тип. Но нет — фактически это будет default(byte[]). Компилятор берёт тип не из левой части, а из веток switch и пытается вывести общий тип. В данном случае общий тип — byte[], значит и default станет byte[].

Для этого конкретного примера разницы нет — и default(byte[]), и default(ReadOnlyMemory<byte>) в итоге дадут совместимый результат. Но последствия могут всплыть позже. Например, если заменить тип на nullable:

var sample = new object();
ReadOnlyMemory<byte>? foo = sample switch
{
byte[] value => value,
_ => default
};


Тип default снова выводится как default(byte[]), то есть null. А дальше начинает работать неявное преобразование:

// из исходников .NET 9
public static implicit operator ReadOnlyMemory<T>(T[]? array)
=> new ReadOnlyMemory<T>(array);


Конструктор ReadOnlyMemory<T>, который принимает T[]?, при null выдаёт пустое значение:

// из исходников .NET 9
public ReadOnlyMemory(T[]? array)
{
if (array == null)
{
this = default;
return;
}

}


В итоге foo окажется пустым ReadOnlyMemory<byte> — не null. То есть foo.HasValue будет true.

Если же явно указать тип в default:

object sample = new();

ReadOnlyMemory<byte>? foo = sample switch
{
byte[] value => value,
_ => default(ReadOnlyMemory<byte>?)
};


То результатом switch будет именно null, и foo.HasValue станет false.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
62
Самые частые способы уменьшить размер Docker-образа

Сохраните себе — это реально частый вопрос на собесах.

• Использовать минимальную базу
Брать alpine, distroless или slim, а не полноценные образы с целой ОС.

• Multi-stage сборка
Собрать всё тяжёлое и зависимости в одном этапе, а в финальный образ скопировать только готовые артефакты.

• Меньше слоёв
Группировать команды в один RUN, когда это уместно, и не плодить лишние слои.

• Убирать зависимости для сборки
Компиляторы, менеджеры пакетов и прочий мусор — только на build-стейдже, не в рантайме.

• Чистить кеш пакетного менеджера
Чистить apt, yum, apk и прочее в том же слое, где ставятся пакеты.

• .dockerignore
Не тащить лишнее в образ: .git, тесты, доки, локальные конфиги и т.д.

• Не копировать всё подряд
Вместо COPY . . копировать только то, что реально нужно для приложения.

• Меньше и легче рантайм
Статические бинарники, минимальные рантаймы, без лишних зависимостей.

• Чистить бинарники и ресурсы
Убирать отладочную информацию, неиспользуемые ассеты и т.п.

• Настраивать права сразу
Ставить chmod/chown рано, чтобы не плодить лишние слои с изменёнными разрешениями.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥63👍1
8 Распространённых Проблем Проектирования Систем и их Решения

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍94🔥2
Уходим от анемичных моделей. Пример DDD-рефакторинга.

Если работали с древней кодовой базой на C#, то наверняка видели анемичную доменную модель. Открываешь какой-нибудь OrderService и ловишь фейспалм: в одном классе и ценообразование, и скидки, и проверка склада, и работа с БД. Оно вроде крутится, пока продукт маленький, но дальше каждая новая фича превращается в рулетку с регрессиями. Тесты тоже летят к черту, потому что доменная логика спрятана под инфраструктурой.

Классика анемичного домена: сущности как DTO с геттерами и сеттерами, а вся логика в сервисах. Понимать систему тяжело, любое изменение наугад, а поведение расползлось по слоям.

В этой серии мы:

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

Старт. Божественный OrderService

Ниже довольно жизненный пример. Помимо подсчета итоговой суммы он:

* навешивает VIP-скидку 5 процентов
* падает, если товара нет на складе
* отклоняет заказ, если клиент вышел за кредитный лимит

// OrderService.cs
public void PlaceOrder(
Guid customerId,
IEnumerable<OrderItemDto> items)
{
var customer = _db.Customers.Find(customerId);
if (customer is null)
throw new ArgumentException("Клиент не найден");

var order = new Order { CustomerId = customerId };

foreach (var dto in items)
{
var inventory = _invService.GetStock(dto.ProductId);
if (inventory < dto.Quantity)
throw new InvalidOperationException("Товара недостаточно");

var price = _pricingService.GetPrice(dto.ProductId);
var lineTotal = price * dto.Quantity;

if (customer.IsVip)
lineTotal *= 0.95m;

order.Items.Add(new OrderItem
{
ProductId = dto.ProductId,
Quantity = dto.Quantity,
UnitPrice = price,
LineTotal = lineTotal
});
}

order.Total = order.Items.Sum(i => i.LineTotal);

if (customer.CreditUsed + order.Total > customer.CreditLimit)
throw new InvalidOperationException("Кредитный лимит превышен");

_db.Orders.Add(order);
_db.SaveChanges();
}


Что здесь рыхлое

1. Правила размазаны по сервису. Скидки, склад, кредит — всё в кучу. Чтобы понять бизнес-логику, нужно разбирать инфраструктурный код.
2. Жесткая связка. OrderService тащит в себя знание о ценах, остатках и EF Core, хотя цель — просто разместить заказ.
3. Больно тестировать. Для юнит-теста нужно мокать БД, ценообразование, склад и сценарии для VIP и не VIP. Тесты получаются хрупкие и громоздкие.

Наша цель — втащить правила в домен, чтобы приложение занималось оркестрацией, а не бизнес-логикой.

Принципы рефакторинга

1. Инварианты держим рядом с данными. Проверки склада, скидки, кредит — в агрегате Order, а не в сервисе сверху.
2. Слой приложения читался как сценарий. Хочется "разместить заказ", а не "подсчитать сумму, проверить лимит, сохранить в БД".
3. Рефакторим кусками. Маленькие шаги, компилируемые на каждом этапе. Никаких переписываний с нуля.
4. Баланс чистоты и практичности. Меняем только там, где выигрыш в читабельности, безопасности и тестируемости действительно окупает вложения.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
12