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

Связь: @devmangx

РКН: https://clck.ru/3FocB6
Download Telegram
Вы всё ещё вызываете 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
Настоятельно рекомендую фри PDF-книгу:

Algorithms от Jeff Erickson — с кучей наглядных иллюстраций и упражнений.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8😁1
Создание параметризованных тестов в xUnit

xUnit использует атрибуты для объявления тестовых методов.
Атрибут Fact описывает обычный тест, а Theory — параметризованный.

Допустим, у нас есть тест, который проверяет, что email-парсер корректно извлекает домен:

public void Should_Return_Domain(
string email, string expectedDomain)
{
var parser = new EmailParser();
var domain = parser.GetDomain(email);
Assert.Equal(domain, expectedDomain);
}


Рассмотрим четыре способа написания параметризованных тестов.

1. InlineData

Самый простой вариант — передать тестовые данные напрямую через атрибут:

[Theory]
[InlineData("test@test.com", "test.com")]
[InlineData("user@github.io", "github.io")]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }


В InlineData мы передаём значения аргументов тестируемого метода и ожидаемый результат. Атрибут можно указывать сколько угодно раз — по одному на каждый тестовый кейс.

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

2. MemberData

MemberData позволяет программно задавать тестовые данные через статическое свойство или метод:

[Theory]
[MemberData(nameof(EmailTestData))]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }

public static IEnumerable<object[]>
EmailTestData => new List<object>
{
new object[] { "test@test.com", "test.com" },
new object[] { "user@github.io", "github.io" }
};


В атрибуте MemberData указывается имя члена. Ограничение здесь одно — свойство или метод должны возвращать IEnumerable<object[]>, из-за чего строгая типизация отсутствует.

3. ClassData

ClassData выносит тестовые данные в отдельный класс. Это удобно для структурирования данных и повторного использования.

Тестовые данные загружаются из класса, который реализует IEnumerable<object[]>:

[Theory]
[ClassData(typeof(EmailTestData))]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }

public class EmailTestData : IEnumerable<object[]>
{
public IEnumerable<object[]> GetEnumerator()
{
yield return new object[] {
"test@test.com", "test.com" };
yield return new object[] {
"user@github.io", "github.io" };
}

IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
}


Подход более громоздкий — нужно вручную реализовывать IEnumerable. При этом проблема с типобезопасностью всё ещё остаётся.

4. TheoryData

TheoryData позволяет хранить тестовые данные с сохранением строгой типизации.

Пример с использованием ClassData (аналогично можно применять и с MemberData, возвращая TheoryData из свойства или метода):

[Theory]
[ClassData(typeof(EmailTestData))]
public void Should_Return_Domain(
string email, string expectedDomain)
{ … }

public class EmailTestData :
TheoryData<string, string>
{
public EmailTestData()
{
Add("test@test.com", "test.com");
Add("user@github.io", "github.io");
}
}


TheoryData — это обобщённый класс, в котором явно задаются типы параметров теста.
Для добавления одного тестового кейса вызывается Add, а новые сценарии просто добавляются дополнительными вызовами этого метода.

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥31👍1
This media is not supported in your browser
VIEW IN TELEGRAM
Это аккуратный и чистый способ работать с Git из терминала

Froggit — минималистичный терминальный UI для Git. Он помогает стейджить файлы, делать коммиты, управлять ветками и смотреть логи без необходимости заучивать сложные команды.

Всё управляется с клавиатуры, работает быстро и без лишних отвлекающих элементов, поэтому проще оставаться в потоке при работе с Git.

Тестим и оцениваем 😁

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4👏2
Разбираем генераторы исходного кода.

Генераторы исходного кода, появившиеся в C# 9, стали мощным инструментом метапрограммирования в .NET. Они позволяют разработчикам автоматически генерировать дополнительный C#-код. Генераторы выполняются на этапе компиляции: анализируют существующий код и создают новые файлы исходников. Эти файлы затем компилируются вместе с остальной кодовой базой, что даёт возможность динамически расширять проект на основе уже написанного кода.

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

В C# 12 генераторы получили дальнейшее развитие. Появилась поддержка более сложных сценариев и выросла производительность за счёт уменьшения шаблонного кода и более эффективных проверок на этапе компиляции.

1. Инкрементные генераторы

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

2. Анализ зависимостей исходного кода

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

3. Улучшенная диагностика

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

4. Улучшения API Roslyn

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

Простой пример генератора кода

1. Настройка проекта

Создадим проект библиотеки и установим необходимые NuGet-пакеты:

dotnet new classlib -n MySourceGenerator
cd MySourceGenerator
dotnet add package Microsoft.CodeAnalysis.CSharp


2. Генератор кода

Нам понадобится класс, реализующий интерфейс IIncrementalGenerator:

using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

[Generator]
public class HelloWorldGenerator : IIncrementalGenerator
{
public void Initialize(
IncrementalGeneratorInitializationContext context)
{
var src =
"""
using System;
namespace HelloGenerated;
public static class HelloWorld
{
public static void SayHello()
=> Console.WriteLine("Hello from the generated code!");
}
""";

context.RegisterPostInitializationOutput(
ctx => ctx.AddSource(
"HelloWorldGenerated",
SourceText.From(src, Encoding.UTF8)));
}
}


Этот генератор добавляет в проект класс HelloWorld с методом SayHello. Сгенерированный код компилируется вместе с основным проектом, поэтому метод HelloGenerated.HelloWorld.SayHello() можно вызывать напрямую из вашего кода.

3. Интеграция с проектом

Создадим консольное приложение:

dotnet new console -n UseSourceGenerator


Добавим ссылку на проект генератора:

<ProjectReference
Include="..\MySourceGenerator\MySourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />


Обратите внимание на два нестандартных атрибута:

OutputItemType="Analyzer" — указывает, что проект должен использоваться как анализатор;

ReferenceOutputAssembly="false" — гарантирует, что целевой проект не будет ссылаться на DLL генератора во время компиляции.

После этого сгенерированный класс HelloWorld станет доступен в консольном проекте.

4. Сборка и запуск

Соберём проект и запустим его. В консоли появится вывод из сгенерированного метода:

// Program.cs
HelloGenerated.HelloWorld.SayHello();


В след. посте про чтение кода из файла 😌

👉 @KodBlog
Please open Telegram to view this post
VIEW IN TELEGRAM
8👍1🤔1
This media is not supported in your browser
VIEW IN TELEGRAM
Это рай для программистов: репозиторий с сотнями бесплатных API.

Отлично подходит для практики и прокачки навыков программирования

Забираем здесь 🥶

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