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

Связь: @devmangx

РКН: https://clck.ru/3FocB6
Download Telegram
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
C# Portal | Программирование
Уходим от анемичных моделей. Пример DDD-рефакторинга. Если работали с древней кодовой базой на C#, то наверняка видели анемичную доменную модель. Открываешь какой-нибудь OrderService и ловишь фейспалм: в одном классе и ценообразование, и скидки, и проверка…
Пример DDD-рефакторинга.

Пошаговый рефакторинг


Цель здесь не в догонялках за академической чистотой или «идеальным» DDD. Идея в том, чтобы постепенно усилить связность и дать домену пространство для нормального выражения своей логики. На каждом шаге задаём простой вопрос: это поведение должно жить в домене? Если да — переносим его туда.

Внедрение логики создания и проверки

Первый шаг — заставить агрегат сам собирать себя. Статический метод Create становится единой точкой входа, где инварианты сразу проверяются по принципу fail fast.

Проверка остатков прямо в Order делает код тестируемее, но жёстко привязывает поток создания заказа к доступности товара. Не всегда это хорошо: в некоторых доменах правильнее оформлять это через доменное событие и проверять асинхронно.

// Order.cs (фабричный метод)

public static Order Create(
Customer customer,
IEnumerable<(Guid productId, int quantity)> lines,
IPricingService pricingService,
IInventoryService invService)
{
var order = new Order(customer.Id);

foreach (var (id, quantity) in lines)
{
if (invService.GetStock(id) < quantity)
throw new InvalidOperationException("Товара недостаточно");

var price = pricingService.GetPrice(id);
order.AddItem(id, quantity, price, customer.IsVip);
}

order.EnsureCreditWithinLimit(customer);

return order;
}


Зачем это всё?

Теперь заказ сразу падает с ошибкой, если нарушен какой-то инвариант. Сервис больше не занимается микроменеджментом скидок или склада — всё это живёт внутри домена.

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

Замечание о передаче сервисов в методы домена

Передача зависимостей вроде IPricingService или IInventoryService в доменный метод Order.Create может выглядеть нестандартно, но это осмысленное решение. Так мы оставляем оркестрацию в домене, а не превращаем app-сервис в procedural god-object.

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

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
8👍3🤔1
Современные Сетевые Сервисы

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
8❤‍🔥2
Принёс вам классный ресурс для изучения паттернов проектирования

На сайте Refactoring Guru собрали примеры для каждого паттерна на таких языках, как C#, Java, Python, PHP, Rust и ещё куча других.

Примеры суперпонятные, с кодом и пояснениями.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍11
Тестирование текущего времени с помощью TimeProvider и FakeTimeProvider

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

В этом посте разберем класс TimeProvider, как его использовать и как подменять в тестах.

Раньше: ручной интерфейс

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

public interface IDateTimeWrapper
{
DateTime GetCurrentDate();
}


И его стандартную реализацию, которая возвращает время в UTC:

public class DateTimeWrapper : IDateTimeWrapper
{
public DateTime GetCurrentDate()
=> DateTime.UtcNow;
}


Либо похожий вариант через абстрактный класс:

public abstract class DateTimeWrapper
{
public virtual DateTime GetCurrentDate() => DateTime.UctNow;
}


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

Сейчас: класс TimeProvider

Начиная с .NET 8, команда .NET добавила абстрактный класс TimeProvider. Он не только даёт абстракцию над локальным временем, но и предоставляет методы для работы с высокоточными временными метками и часовыми поясами.

Важно: даты возвращаются в виде DateTimeOffset, а не DateTime.

TimeProvider доступен из коробки в консольных приложениях .NET:

DateTimeOffset utc = TimeProvider.System.GetUtcNow();
Console.WriteLine(utc);

DateTimeOffset local = TimeProvider.System.GetLocalNow();
Console.WriteLine(local);


Если вы используете dependency injection, его достаточно зарегистрировать как синглтон:

builder.Services.AddSingleton(TimeProvider.System);


Пример использования:

public class Vacation(TimeProvider _time)
{
public bool IsVacation
=> _time.GetLocalNow().Month == 8;
}


Тестирование TimeProvider

Для тестов можно использовать NuGet-пакет Microsoft.Extensions.TimeProvider.Testing, в котором есть класс FakeTimeProvider. Это заглушка для абстрактного TimeProvider.

С помощью FakeTimeProvider можно вручную задавать текущее UTC-время, локальное время и другие параметры, которые предоставляет TimeProvider:

[Fact]
public void WhenItsAugust_ShouldReturnTrue()
{
// Arrange
var fakeTime = new FakeTimeProvider();
fakeTime.SetUtcNow(
new DateTimeOffset(2025, 8, 14,
22, 24, 12, TimeSpan.Zero));
var sut = new Vacation(fakeTime);

Assert.True(sut.IsVacation);
}


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

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