23284
Все самое полезное для C#-разработчика в одном канале. По рекламе: @proglib_adv Учиться у нас: https://proglib.io/w/b60af5a4 Для обратной связи: @proglibrary_feeedback_bot РКН: https://gosuslugi.ru/snet/67a5c81cdc130259d5b7fead
📎 Union-типы в действии
Мы уже рассмотрели что такое union-тип, теперь пора подумать где это использовать.
Бывает, что API принимает как одиночное значение, так и коллекцию. Union с телом позволяет добавить вспомогательный метод прямо в объявление:
public union OneOrMore<T>(T, IEnumerable<T>)
{
public IEnumerable<T> AsEnumerable() => Value switch
{
T single => [single],
IEnumerable<T> multiple => multiple,
null => []
};
}
OneOrMore<string> tags = "dotnet";
OneOrMore<string> moreTags = new[] { "csharp", "unions", "preview" };
foreach (var tag in tags.AsEnumerable())
Console.Write($"[{tag}] ");
// [dotnet]
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;
public interface IUnion
{
object? Value { get; }
}
}
👀 C# 15 Union Types: наконец-то закрытые типы в языке
В C# 15 появился ключевой синтаксис union. Он решает давнюю проблему: когда метод должен вернуть одно из нескольких возможных значений, раньше выбор был невелик. object не накладывает никаких ограничений, маркерные интерфейсы нельзя «запечатать», а базовые классы требуют общего предка. Union types убирают все эти ограничения.
Что это и как работаетUnion-тип объявляет закрытое множество допустимых типов. Компилятор знает полный список, поэтому проверяет исчерпываемость switch-выражений прямо при сборке.
Простейший пример:
public record class Cat(string Name);
public record class Dog(string Name);
public record class Bird(string Name);
public union Pet(Cat, Dog, Bird);
Pet pet = new Dog("Rex");
Console.WriteLine(pet.Value); // Dog { Name = Rex }switch по такой переменной не требует ветки default или _. Если вы позже добавите четвёртый тип в объявление union, компилятор выдаст предупреждение в каждом месте, где не хватает обработчика.string name = pet switch
{
Dog d => d.Name,
Cat c => c.Name,
Bird b => b.Name,
};
👀 Разрешение перегрузок в C#
Фрагмент кода выглядит как задача с подвохом. Два метода, оба принимают null. Какой вызовется:
void Print(string text) => Console.WriteLine("String");
void Print(object obj) => Console.WriteLine("Object");
Print(null);null совместим и со string, и с object, потому что оба являются ссылочными типами и принимают null. Выбор делается по принципу наибольшей специфичности: из нескольких подходящих перегрузок выбирается та, чей параметр является более производным типом. string наследует от object, значит string более специфичный тип. null к нужному типу: Print((object)null); // выведет "Object"
Print((string)null); // выведет "String"
💡 Фиксируйте архитектурные решения прямо в репозитории
Architectural Decision Records это короткие Markdown-файлы, которые фиксируют контекст, само решение и последствия. Не многостраничная спецификация, а что-то ближе к протоколу встречи. Читается за минуты, но объясняет «почему» лучше любого комментария в коде.
Проблема большинства существующих инструментов для ADR в том, что шаблоны зашиты в сам инструмент. Поменяла команда подход к документированию, нужно ставить другой инструмент.
dotnet-adr это .NET Global Tool, который отделяет сам инструмент от шаблонов. Шаблоны живут как NuGet-пакеты: их можно менять, публиковать свои и раздавать внутри организации через приватный feed.
Установка:
dotnet tool install -g adr
adr templates package set adr.templates
adr templates package install
adr new "Use PostgreSQL instead of MongoDB"
{
"path": "./Docs/Adr"
}adr new "Switch to Cosmos DB" -i 3
🤩 Подборка вакансий для шарпистов
C# Backend Developer — от 180 000 ₽ гибрид в Санкт-Петербурге
Unity разработчик — до 4 500 €, гибрид в Алматы
Fullstack-разработчик (C# / React Native) — удалёнка или гибрид в Пензе
➡️ Еще больше топовых вакансий — в нашем канале C# Jobs
🐸 Библиотека шарписта
🚩 OpenFeature для .NET
Смена провайдера feature flags обычно означает переписывание интеграции. OpenFeature это открытый стандарт под крылом CNCF, который даёт единый vendor-agnostic API: меняете провайдера, меняете одну строчку, код не трогаете.
Установка
dotnet add package OpenFeature
await Api.Instance.SetProviderAsync(new InMemoryProvider());
var client = Api.Instance.GetClient();
bool v2Enabled = await client.GetBooleanValueAsync("v2_enabled", false);
if (v2Enabled)
{
// новая логика
}
// Глобально
EvaluationContext ctx = EvaluationContext.Builder()
.Set("region", "us-east-1")
.Build();
Api.Instance.SetContext(ctx);
// Или прямо в вызове
bool flagValue = await client.GetBooleanValueAsync(
"some-flag", false, reqCtx);
// Глобально для всех вызовов
Api.Instance.AddHooks(new ExampleGlobalHook());
// Только для конкретного клиента
client.AddHooks(new ExampleClientHook());
LoggingHook пишет детальные логи через Microsoft.Extensions.Logging.Api.Instance.AddHandler(
ProviderEventTypes.ProviderReady,
(eventDetails) => Console.WriteLine(eventDetails.Type)
);
ProviderReady, ProviderError, ProviderConfigurationChanged.dotnet add package OpenFeature.Hosting
builder.Services.AddOpenFeature(featureBuilder => {
featureBuilder
.AddInMemoryProvider()
.AddHook<LoggingHook>();
});var multiProvider = new MultiProvider(providerEntries, new FirstMatchStrategy());
await Api.Instance.SetProviderAsync(multiProvider);
public class MyProvider : FeatureProvider
{
public override Metadata GetMetadata() =>
new Metadata("My Provider");
public override Task<ResolutionDetails<bool>> ResolveBooleanValueAsync(
string flagKey, bool defaultValue,
EvaluationContext? context = null, ...)
{
// ваша логика
}
// + ResolveString, ResolveInteger, ResolveDouble, ResolveStructure
}
Api.Instance.SetTransactionContextPropagator(
new AsyncLocalTransactionContextPropagator());
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#garbage_collector
💡 Красивые алгоритмы медленны при малом n
Красивые алгоритмы с хорошей асимптотикой имеют большие константы. O(log n) звучит лучше O(n), но если n=20 — линейный поиск по массиву быстрее бинарного поиска по дереву просто потому, что данные помещаются в кэш процессора и нет накладных расходов на обход структуры.
Допустим, нужно найти обработчик по типу события. Первый импульс это словарь или дерево:
// "Правильное" решение — O(1) lookup
private readonly Dictionary<string, IHandler> _handlers = new()
{
["OrderCreated"] = new OrderCreatedHandler(),
["OrderCancelled"] = new OrderCancelledHandler(),
["OrderShipped"] = new OrderShippedHandler(),
};
// "Наивное" решение — O(n) linear scan
private readonly (string EventType, IHandler Handler)[] _handlers =
[
("OrderCreated", new OrderCreatedHandler()),
("OrderCancelled", new OrderCancelledHandler()),
("OrderShipped", new OrderShippedHandler()),
];
public IHandler? Find(string eventType)
{
foreach (var (type, handler) in _handlers)
if (type == eventType) return handler;
return null;
}
[MemoryDiagnoser]
public class LookupBenchmark
{
private readonly Dictionary<string, int> _dict;
private readonly (string, int)[] _array;
public LookupBenchmark()
{
var data = Enumerable.Range(0, 10)
.Select(i => ($"key{i}", i))
.ToArray();
_dict = data.ToDictionary(x => x.Item1, x => x.Item2);
_array = data;
}
[Benchmark(Baseline = true)]
public int DictLookup() => _dict["key7"];
[Benchmark]
public int ArrayScan()
{
foreach (var (k, v) in _array)
if (k == "key7") return v;
return -1;
}
}
📰 Дайджест недели
Последний дайджест марта.
— Generative AI for Beginners .NET v2
— Почти год с Copilot Coding Agent в dotnet/runtime
— Пять типичных ошибок при проектировании интеграции с помощью Kafka
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#async_news
⚡️ Рестарт ради смены настроек это лишнее
Если конфигурация меняется редко, перезапуск приложения — не проблема. Но когда нужно менять, например, тарифы или флаги в реальном времени, рестарт становится дорогим решением. IOptionsMonitor<T> позволяет получать актуальные значения сразу после изменения файла конфигурации.
Как это работаетIOptionsMonitor<T> следит за изменениями источника конфигурации. При каждом обращении к CurrentValue возвращается актуальное значение. Дополнительно можно подписаться на событие изменения через OnChange:
public class DynamicPricingService
{
private readonly IOptionsMonitor<PricingOptions> _options;
public DynamicPricingService(IOptionsMonitor<PricingOptions> options)
{
_options = options;
_options.OnChange(updatedOptions =>
{
Log.Information("Pricing updated: BaseRate={BaseRate}",
updatedOptions.BaseRate);
});
}
public decimal CalculatePrice(decimal distance)
{
var currentOptions = _options.CurrentValue;
return currentOptions.BaseRate + (distance * currentOptions.PerMileRate);
}
}
CalculatePrice берёт свежее значение из CurrentValue без рестарта и без ручного сброса кэша. Регистрация в Program.cs:builder.Services.AddOptions<PricingOptions>()
.BindConfiguration("Pricing", binderOptions =>
{
binderOptions.BindNonPublicProperties = false;
binderOptions.ErrorOnUnknownConfiguration = true;
})
.ValidateDataAnnotations();
ErrorOnUnknownConfiguration = true защищает от опечаток в ключах — неизвестное поле в конфиге вызовет ошибку, а не тихо проигнорируется.IOptionsMonitor<T> — синглтон. Одно и то же значение живёт на протяжении всего времени работы приложения и обновляется при изменении файла.IOptionsSnapshot<T> — скоупед. Значение фиксируется один раз на запрос и не меняется до его завершения. Это важно там, где нужна консистентность внутри одного HTTP-запроса — чтобы один и тот же запрос не увидел разные значения конфигурации в начале и в конце обработки.IOptionsMonitor. Если важна согласованность в рамках запроса, IOptionsSnapshot.
✏️ Задание: не положить базу после истечения TTL
Представьте: TTL кэша истёк, и сотни запросов одновременно обнаружили пустой кэш. Все ломятся в базу за одним и тем же значением. Это называется cache stampede.
Как бы вы это решили? Какой примитив синхронизации выбрать, чтобы первый запрос шёл в БД, а остальные ждали его результата?
Подумайте и проверьте свой ответ здесь: @csharp_interview_lib
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#dotnet_challenge
😱 Если ваш продукт не умеет отдавать данные в формате, понятном AI-агенту, то вас просто не существует
Скрипт не будет кликать по красивым кнопкам в браузере, он уйдёт к конкуренту с нормальным API. Перестроить архитектуру под машинных клиентов — это уже не хайп, а необходимое условие сохранения конкурентоспособности.
Как адаптировать продукт и не исчезнуть из выдачи:
— интегрировать MCP и A2A-взаимодействие, чтобы агенты могли вас читать;
— научиться контролировать стоимость (лимиты, кэш, роутинг между моделями);
— настроить AgentOps: трейсинг, логирование и отлов регрессий.
Всё это ждёт вас на обновлённом курсе «Разработка AI-агентов». Мы специально сделали фокус на утилитарном инжиниринге и production-ready решениях.
Кстати, до 29 марта можно забрать курс с большой скидкой, и стоит поторопиться — мест на потоке всё меньше.
Зафиксировать цену и начать деплоить агентов без слива бюджета 👈
🕸 Пять типичных ошибок при проектировании интеграции с помощью Kafka
Система работала нормально, пока обработка батча не заняла слишком много времени. Координатор консьюмер-групп решил, что консьюмер мёртв, отверг коммит оффсета — и консьюмер начал читать сообщения заново. По кругу.
Это называется ложная ребалансировка, и это один из самых неочевидных сценариев поломки.
Архитектор Альфа-Банка разобрал пять таких историй из реального прода — с объяснением механики и тем, как этого избежать.
➡️ Читать статью
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
🔎 Удалённое комбо вакансий для шарпистов
Подобрали три вакансии для тех, кто не хочет выходить из дома:
.NET-разработчик
C#/.NET-разработчик
Senior Backend .NET Engineer
➡️ Еще больше топовых вакансий — в нашем канале C# Jobs
🐸 Библиотека шарписта
🤨 10 месяцев с Copilot Coding Agent в dotnet/runtime
Microsoft опубликовали детальный разбор использования GitHub Copilot Coding Agent в репозитории dotnet/runtime за последние 10 месяцев. Это один из самых сложных проектов в мире: основа .NET, 14 миллионов строк кода, миллионы активных разработчиков. Репозиторий использовали как полигон для эксперимента: может ли AI-агент полноценно участвовать в разработке такого масштаба?
Общая картина: 878 PR за 10 месяцев
С мая 2025 по март 2026 команда открыла 878 PR через CCA. Из них принято 535 (67.9%), закрыто 253, ещё 90 остаются открытыми.
Для сравнения: PR от разработчиков Microsoft принимаются в 87.1% случаев, от сообщества — в 79.7%, от ботов вроде dependabot — в 85.9%. CCA уступает людям по этому показателю, но авторы подчёркивают: это разные задачи.
Важный показатель качества — процент откатов. Из 535 принятых PR через CCA откатили 3 (0.6%). Среди остальных PR — 33 из 4251 (0.8%). Разница статистически незначима, но это говорит о том, что AI-код не хуже по качеству в плане регрессий.
Главный урок: инструкции важнее модели
До появления файла .github/copilot-instructions.md успешность выполнения задач была 38%. После — 69%. Это самый показательный факт во всей статье.
Изначально CCA не мог скачать NuGet-пакеты, потому что firewall блокировал внешние ресурсы. Не знал, как собирать репозиторий. Не мог запустить тесты. Это было похоже на то, как взять нового разработчика и поставить ему задачу, не дав доступа ни к чему.
Основные выводы авторов: думайте об агенте как о паре, а не замене; задачи с чётким описанием и понятным скоупом — лучшие кандидаты; ожидайте несколько итераций; следите за «ленивостью» агента, который делает минимум и не экстраполирует; каждый урок, вложенный в файл инструкций, окупается на всех следующих PR.
➡️ Блог разработчиков
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#async_news
✏️ Перегрузка и переопределение
На технических интервью по C# для джунов и иногда для мидлов вопрос про перегрузки и переопределения задаётся одним из первых.
Оба механизма связаны с методами и их именами. Оба выглядят похоже на первый взгляд. Но работают они в совершенно разных ситуациях и решают разные задачи.
Разница между ними принципиальная. Один работает на уровне компиляции, другой на уровне выполнения программы. Один не требует наследования, другой без него невозможен.
➡️ Как ответить на собесе
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#dotnet_challenge
👨💻 Баги, которые уничтожат ваших пользователей
Большинство проблем с многопоточностью выглядят одинаково везде. Но часть из них появляется только в конкретной среде. WinForms, WPF и ASP.NET имеют свои контексты синхронизации, и если их игнорировать, получаем краш или дедлок там, где вроде бы всё выглядело нормально.
Что идёт не так
В WinForms и WPF UI-компоненты не являются потокобезопасными. Обновлять их можно только из UI-потока. Если фоновый поток попытается напрямую записать что-то в label.Text или textBox.Value, получаем InvalidOperationException. В WPF для этого используется Dispatcher.BeginInvoke, в WinForms — Control.Invoke или Control.BeginInvoke.
Казалось бы, очевидное правило. Но баг всё равно появляется: чаще всего тогда, когда разработчик делает Task.Run, внутри него обращается к UI, а анализатор это не видит.
С Dispatcher.Invoke другая история. Это синхронный вызов, он блокирует текущий поток до завершения. Если вызвать Invoke из самого UI-потока или из кода, который UI-поток уже ждёт, получаем дедлок. Правило простое: почти всегда нужен BeginInvoke (асинхронный), а не Invoke.
Как находить такие баги до прода
Статические анализаторы: Roslyn, AsyncFixer, Microsoft.VisualStudio.Threading.Analyzers и ThreadSafetyAnalyzer умеют находить прямые обращения к UI из неправильного потока, синхронные блокировки async-методов и потенциальные дедлоки в диспетчере.
Подключить их можно через NuGet:
dotnet add package Microsoft.VisualStudio.Threading.Analyzers
dotnet add package AsyncFixer
✌🏻 У нас две новости — хорошая и плохая!
Хорошая: Ваших знаний, скорее всего, хватит, чтобы собрать рабочую демку AI-агента в Colab. 🫡
Плохая: Вы вряд ли выведете его в прод, не обанкротившись на токенах и не слив базу. 🤯
Для защиты от таких сценариев мы полностью пересобрали курс «Разработка AI-агентов». Теперь внутри плотная работа с экономикой ресурсов, дебаг через time-travel в LangGraph, извлечение данных из кривых сканов для RAG и комплаенс по 152-ФЗ.
Если всё ещё сомневаетесь, послушайте голосовое от спикера курса Влада Прошинского, где он объясняет, как правильно тестировать агентов перед релизом.
📎 Task.Run внутри ASP.NET пайплайна
Один из самых распространённых антипаттернов в .NET, который выглядит как хорошая практика, но на деле замедляет систему.
Обычный код:
await Task.Run(() => _logger.LogInformation("Processing..."));
await Task.Run(() => MapToDto(entity));
await Task.Run(() => ValidateHeaders(request));Task.Run внутри ASP.NET запроса:// Логирование — всегда синхронно
_logger.LogInformation("Processing...");
// Маппинг — синхронно
var dto = MapToDto(entity);
// Валидация заголовков — синхронно
ValidateHeaders(request);
// async оставляем только для реального I/O
var data = await _repository.GetAsync(id);
var response = await _httpClient.GetAsync(url);
⚡️ Никаких больше var
Microsoft официально объявила: в C# 15 ключевое слово var признаётся устаревшим.
Команда языка ссылается на исследования читаемости кода: оказывается, явное указание типов снижает когнитивную нагрузку на 34% и ускоряет код ревью. Roslyn уже умеет автоматически выводить тип, но теперь хочет, чтобы это делал и программист.
Миграция через dotnet-upgrade-assistant проставит типы автоматически. Но 40 000 строк кода всё равно ждут вас в ближайшем будущем.
➡️ Источник
Попались? С первым апреля!😁
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
🚫 Span<T> и async несовместимы Span<T> это ref struct. А ref struct не может существовать в куче. Это не ограничение реализации, это гарантия безопасности по дизайну.
Async-методы компилятор превращает в state machine — объект, который живёт в куче и может приостанавливаться между await-точками. Локальные переменные такого метода становятся полями этого объекта. Поле типа ref struct в объекте на куче — запрещено. Поэтому компилятор просто не даст использовать Span<T> в async-методе.
// Не скомпилируется
async Task ProcessAsync(byte[] data)
{
Span<byte> span = data; // CS4012: Span нельзя использовать в async
await Task.Delay(100);
Process(span);
}
// Упрощённо — что генерирует компилятор
private struct ProcessAsyncStateMachine : IAsyncStateMachine
{
public byte[] data;
public Span<byte> span; // ← невозможно: ref struct не может быть полем
public int _state;
// ...
}
await не гарантирован, потому что поток может смениться, метод может возобновиться на другом потоке. Span на стеке к тому моменту уже не существует.Memory<T> — это то, для чего он и создан. Может жить в куче, передаётся через await, конвертируется в Span в синхронных участках:async Task ProcessAsync(Memory<byte> memory)
{
await Task.Delay(100); // можно
// Span получаем только там, где нет await
Span<byte> span = memory.Span;
Process(span);
}
Memory<T> для хранения и передачи через async-границы, Span<T> для фактической работы с данными в синхронном контексте.async Task<int> ReadAndProcessAsync(Stream stream)
{
// Memory живёт в куче — await доволен
var buffer = new byte[4096];
Memory<byte> memory = buffer;
int bytesRead = await stream.ReadAsync(memory);
// Переходим в sync-контекст — достаём Span
Span<byte> span = memory.Span[..bytesRead];
return CountNewlines(span);
}
static int CountNewlines(Span<byte> data)
{
int count = 0;
foreach (var b in data)
if (b == '\n') count++;
return count;
}
Span<T> — инструмент для горячего пути в синхронном коде. Как только появляется await переходите на Memory<T> и конвертируйте в Span только там, где он нужен непосредственно для вычислений.
🎶 Разработчик написал программу для управления самодельным проигрывателем винила
Разработчик с Reddit строит автоматический проигрыватель пластинок с нуля: механику, электронику и прошивку для STM32. Чтобы тестировать и отлаживать железо в процессе разработки, он написал десктопное управляющее приложение на C#.
Приложение позволяет управлять проигрывателем с компьютера, снимать статистику и диагностировать проблемы на лету — по сути, это инструментарий для разработчика железа, написанный на том же языке, что и обычный бизнес-софт.
Для него это первый опыт написания control software для физического железа и судя по его словам, ощущение от того, что код управляет реальным устройством в реальном мире, совершенно другое.
➡️ Источник
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#entry_point
👨💻 Разделяемое состояние в многопоточке
Кажется, что примитивы атомарны. Это не так в смысле видимости между потоками: процессор и компилятор переупорядочивают инструкции, каждое ядро держит своё значение в кэше:
private bool _cacheLoaded;
// Поток A
_cacheLoaded = true;
// Поток B — может прочитать false, даже если A уже записал true
if (!_cacheLoaded) LoadCache(); // загружается дважды, данные затираются
lock. Подходит для составных операций: «прочитать → изменить → записать» должны выполняться как одно целое:private readonly object _sync = new object();
private int _count;
public void Increment()
{
lock (_sync) { _count++; }
}
volatile. Запрещает кэширование значения в регистре. Не заменяет lock. Только для простого чтения/записи одного поля без зависимостей от других.private volatile bool _cacheLoaded;
Interlocked. Атомарная операция на уровне процессора. Быстрее lock, но только для простых числовых операций:private int _count;
public void Increment()
{
Interlocked.Increment(ref _count);
}
⚙️ Substring или Slice
Substring и Slice выглядят похоже, но работают принципиально по-разному.
Substring — это new string(...). Каждый вызов:
— выделяет новый объект в хипе
— копирует символы в него
— создаёт нагрузку на GC
Slice не создаёт объектов. Это просто новый указатель + длина поверх той же памяти. int.Parse(ReadOnlySpan<char>) читает символы напрямую оттуда.
Частая ошибка
// Так делать не надо — убивает весь смысл
int id = int.Parse(span.Slice(5, 2).ToString());
ToString() на Span создаёт новую строку. Вернулись к исходной проблеме.Substring перед Parse это кандидат на замену.
⚙️ Покрытие кода для .NET
Coverlet — инструмент для измерения покрытия кода в .NET-проектах. Он работает на Windows, macOS и Linux, поддерживает .NET Framework и .NET Core, и умеет считать покрытие по строкам, ветвям и методам.
Без инструмента покрытия вы пишете тесты вслепую. Можно потратить часы на тесты, которые проверяют одно и то же, и совсем не касаться критических участков кода.
Coverlet показывает точную картину: вот этот метод не вызывается ни одним тестом, а вот эта ветка if никогда не выполняется при тестировании.
Как подключить
Есть четыре варианта интеграции. Самый распространённый для современных проектов через VSTest. Он уже включён по умолчанию в шаблоны xUnit-проектов начиная с .NET 8.
Если его нет, добавляем в тестовый проект:
dotnet add package coverlet.collector
dotnet test --collect:"XPlat Code Coverage"
dotnet add package coverlet.msbuild
dotnet test /p:CollectCoverage=true
dotnet tool install --global coverlet.console
coverlet /path/to/test-assembly.dll --target "dotnet" --targetargs "test /path/to/test-project --no-build"
dotnet add package coverlet.MTP
dotnet test --coverlet
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#garbage_collector
🤔 Запросы в БД быстрые, кэш есть, async везде, но приложение тормозит
Скорее всего виноват не код, а аллокации.
Каждый раз, когда вы пишете Substring(), Split() или ToArray(), то вы создаёте новые объекты на хипе. При большой нагрузке это тысячи объектов в секунду.
А дальше приходит Garbage Collector и делает паузу. Вот откуда те самые загадочные всплески задержек.
Что делать:
→ Используйте Span<T> и ReadOnlySpan<T> вместо substring
→ ArrayPool<T>.Shared.Rent() вместо new массивов
→ StringBuilder пулить через ObjectPool
→ Профилируйте через dotnet-trace + BenchmarkDotNet
Правило простое: меньше объектов на хипе → реже GC → стабильная латентность. Иногда пара изменений убирает 80% тормозов.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#sharp_view
⚙️ yield return не бесплатный
Итераторы выглядят просто, но работают иначе:
IEnumerable<int> GetIds()
{
foreach (var item in _items)
yield return item.Id;
}
yield return это правильный выбор. Код чище, API удобнее, читаемость выше.void FillIds(Span<int> buffer)
{
for (int i = 0; i < _items.Count; i++)
buffer[i] = _items[i].Id;
}
List<int> GetIds()
{
var result = new List<int>(_items.Count);
foreach (var item in _items)
result.Add(item.Id);
return result;
}
for (int i = 0; i < _items.Count; i++)
Process(_items[i].Id);
yield return стоит использовать тогда, когда он делает API лучше и код понятнее. Но не стоит считать его нулевым по стоимости. Если участок горячий, то сначала замерьте, потом решайте.
👨💻 Конфигурация везде это проблема
Когда конфигурация разбросана по всему коду, проект становится хрупким. Строки вроде _configuration["Stripe:ApiKey"] встречаются в контроллерах, сервисах, хелперах — и каждый раз с риском опечатки, без типизации, без валидации. Поменяли ключ в appsettings — ищете, где всё сломалось.
Что не так с таким подходом
Прямое обращение к IConfiguration через магические строки создаёт несколько реальных проблем. Нет единого места, где видно всю конфигурацию приложения. Парсинг типов (int.Parse, bool.Parse) повторяется в разных местах. Опечатка в ключе не вызовет ошибку компиляции — только падение в рантайме.
Тестировать такой код неудобно:
// Так делать не стоит — магические строки, парсинг вручную
var apiKey = _configuration["Stripe:ApiKey"];
var timeout = int.Parse(_configuration["Timeout"] ?? "30");
// Infrastructure/Configuration/ApplicationOptions.cs
public sealed class ApplicationOptions
{
public PaymentOptions Payment { get; set; } = new();
public DatabaseOptions Database { get; set; } = new();
public CacheOptions Cache { get; set; } = new();
public FeatureFlags Features { get; set; } = new();
}
public sealed class PaymentOptions
{
public string StripeApiKey { get; set; } = string.Empty;
public string StripeWebhookSecret { get; set; } = string.Empty;
public int RetryAttempts { get; set; } = 3;
}
Program.cs одна привязка для всего:builder.Services.AddOptions<ApplicationOptions>()
.Bind(builder.Configuration)
.ValidateDataAnnotations()
.ValidateOnStart();
ValidateOnStart выбрасывает исключение при запуске, если конфигурация невалидна. Не в рантайме, не при первом запросе — сразу при старте.Program.cs, выносим регистрацию в extension method и сразу открываем под-опции для инжекции:public static class OptionsExtensions
{
public static IServiceCollection AddApplicationOptions(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddOptions<ApplicationOptions>()
.Bind(configuration)
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton(sp =>
sp.GetRequiredService<IOptions<ApplicationOptions>>().Value.Payment);
services.AddSingleton(sp =>
sp.GetRequiredService<IOptions<ApplicationOptions>>().Value.Database);
return services;
}
}
IOptions<T>. Инжектируем напрямую:public class OrderService
{
private readonly PaymentOptions _paymentOptions;
private readonly DatabaseOptions _dbOptions;
public OrderService(
PaymentOptions paymentOptions,
DatabaseOptions dbOptions)
{
_paymentOptions = paymentOptions;
_dbOptions = dbOptions;
}
}
👨💻 Хватит мучить MongoDB через EF Core
Вы выбрали MongoDB ради гибкости. А потом два часа потратили на то, чтобы EF Core позволил сделать элементарный фильтр. Поздравляем, вы сами себе враг.
EF Core для MongoDB это иллюзия комфорта. Знакомый синтаксис, но в обмен на всё, ради чего вообще брали Mongo: агрегационные пайплайны, геозапросы, полнотекстовый поиск. Провайдер этого просто не умеет.
Правильный путь — это MongoDB.Driver напрямую.
Ставите пакет:
dotnet add package MongoDB.Driver
[CollectionName("orders")]
public class Order
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public string CustomerName { get; set; }
}public class MongoRepository<T> : IMongoRepository<T>
{
private readonly IMongoCollection<T> _collection;
public MongoRepository(IMongoDatabase db)
{
var name = typeof(T)
.GetCustomAttribute<CollectionNameAttribute>()?.Name
?? typeof(T).Name;
_collection = db.GetCollection<T>(name);
}
}
services.AddScoped(typeof(IMongoRepository<>), typeof(MongoRepository<>));
[CollectionName]. var filter = Builders<Order>.Filter.Eq(o => o.Status, "pending");
var sort = Builders<Order>.Sort.Descending(o => o.CreatedAt);
var results = await _collection.Find(filter).Sort(sort).ToListAsync();