csharpproglib | Unsorted

Telegram-канал csharpproglib - Библиотека шарписта | C#, F#, .NET, ASP.NET

23284

Все самое полезное для C#-разработчика в одном канале. По рекламе: @proglib_adv Учиться у нас: https://proglib.io/w/b60af5a4 Для обратной связи: @proglibrary_feeedback_bot РКН: https://gosuslugi.ru/snet/67a5c81cdc130259d5b7fead

Subscribe to a channel

Библиотека шарписта | C#, F#, .NET, ASP.NET

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#garbage_collector

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

📎 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]


Поддержка UnionAttribute и IUnion в рантайме ещё не добавлена, поэтому в Preview 2 нужно вручную добавить в проект небольшой полифил:
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct,
AllowMultiple = false)]
public sealed class UnionAttribute : Attribute;

public interface IUnion
{
object? Value { get; }
}
}


После этого синтаксис union работает в полном объёме.

➡️ Блог разработчиков

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

👀 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 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,
};


Фича доступна начиная с .NET 11 Preview 2.

➡️ Блог разработчиков

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

👀 Разрешение перегрузок в C#

Фрагмент кода выглядит как задача с подвохом. Два метода, оба принимают null. Какой вызовется:

void Print(string text) => Console.WriteLine("String");
void Print(object obj) => Console.WriteLine("Object");

Print(null);


Ответ: выведется "String". И это не случайность, а предсказуемое поведение разрешения перегрузок в C#.

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

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

Чтобы явно указать нужную перегрузку, достаточно привести null к нужному типу:
Print((object)null);  // выведет "Object"
Print((string)null); // выведет "String"


📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

💡 Фиксируйте архитектурные решения прямо в репозитории

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:
adr new "Use PostgreSQL instead of MongoDB"


Инструмент создаст нумерованный Markdown-файл с заголовком и структурой из выбранного шаблона. По умолчанию файлы складываются в docs/adr, но путь настраивается через adr.config.json в корне репозитория:
{
"path": "./Docs/Adr"
}


Если одно решение заменяет другое, это фиксируется явно:
adr new "Switch to Cosmos DB" -i 3


Третий ADR получит статус «superseded», новый сошлётся на него.

Инструмент подходит тем, кто работает в .NET-экосистеме и хочет хранить архитектурные решения рядом с кодом, не усложняя процесс.

➡️ Репозиторий

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

🤩 Подборка вакансий для шарпистов

C# Backend Developer — от 180 000 ₽ гибрид в Санкт-Петербурге

Unity разработчик — до 4 500 €, гибрид в Алматы

Fullstack-разработчик (C# / React Native) — удалёнка или гибрид в Пензе

➡️ Еще больше топовых вакансий — в нашем канале C# Jobs

🐸 Библиотека шарписта

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

🚩 OpenFeature для .NET

Смена провайдера feature flags обычно означает переписывание интеграции. OpenFeature это открытый стандарт под крылом CNCF, который даёт единый vendor-agnostic API: меняете провайдера, меняете одну строчку, код не трогаете.

Установка

dotnet add package OpenFeature


Требования: .NET 8+ или .NET Framework 4.6.2+

Минимальный пример:
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.

Dependency Injection (экспериментально)

dotnet add package OpenFeature.Hosting


builder.Services.AddOpenFeature(featureBuilder => {
featureBuilder
.AddInMemoryProvider()
.AddHook<LoggingHook>();
});


Поддержка domain-scoped провайдеров: разные провайдеры для разных частей приложения.

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

- FirstMatchStrategy — первый ненулевой результат
- FirstSuccessfulStrategy — первый успешный, игнорируя ошибки
- ComparisonStrategy — параллельное выполнение + сравнение результатов

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
}


Для ASP.NET Core один раз настроили контекст на входе запроса, и он автоматически попадает во все вычисления флагов в рамках этого запроса:
Api.Instance.SetTransactionContextPropagator(
new AsyncLocalTransactionContextPropagator());


📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#garbage_collector

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

💡 Красивые алгоритмы медленны при малом 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;
}


При 5–20 обработчиках линейный массив часто быстрее словаря: данные лежат последовательно в памяти, нет хеширования, нет разыменования указателей, кэш доволен. Dictionary начинает выигрывать при десятках тысяч элементов и только тогда.

Бенчмарк говорит сам за себя:
[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;
}
}


При n=10 массив зачастую быстрее и не аллоцирует ничего лишнего. Измерьте сами.

Когда измерения при реальной нагрузке показывают, что n действительно большой и растёт. Не раньше. Routing-таблица с 15 маршрутами, валидация с 8 правилами, матчинг по 12 паттернам — всё это «малый n», и простой цикл здесь выиграет у любого красивого решения.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

📰 Дайджест недели

Последний дайджест марта.

Generative AI for Beginners .NET v2

Почти год с Copilot Coding Agent в dotnet/runtime

Пять типичных ошибок при проектировании интеграции с помощью Kafka

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#async_news

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

⚡️ Рестарт ради смены настроек это лишнее

Если конфигурация меняется редко, перезапуск приложения — не проблема. Но когда нужно менять, например, тарифы или флаги в реальном времени, рестарт становится дорогим решением. 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 против IOptionsSnapshot

IOptionsMonitor<T> — синглтон. Одно и то же значение живёт на протяжении всего времени работы приложения и обновляется при изменении файла.

IOptionsSnapshot<T> — скоупед. Значение фиксируется один раз на запрос и не меняется до его завершения. Это важно там, где нужна консистентность внутри одного HTTP-запроса — чтобы один и тот же запрос не увидел разные значения конфигурации в начале и в конце обработки.

Если сервис живёт в синглтоне, используйте IOptionsMonitor. Если важна согласованность в рамках запроса, IOptionsSnapshot.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

✏️ Задание: не положить базу после истечения TTL

Представьте: TTL кэша истёк, и сотни запросов одновременно обнаружили пустой кэш. Все ломятся в базу за одним и тем же значением. Это называется cache stampede.

Как бы вы это решили? Какой примитив синхронизации выбрать, чтобы первый запрос шёл в БД, а остальные ждали его результата?

Подумайте и проверьте свой ответ здесь: @csharp_interview_lib

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#dotnet_challenge

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

😱 Если ваш продукт не умеет отдавать данные в формате, понятном AI-агенту, то вас просто не существует

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

Как адаптировать продукт и не исчезнуть из выдачи:

— интегрировать MCP и A2A-взаимодействие, чтобы агенты могли вас читать;
— научиться контролировать стоимость (лимиты, кэш, роутинг между моделями);
— настроить AgentOps: трейсинг, логирование и отлов регрессий.

Всё это ждёт вас на обновлённом курсе «Разработка AI-агентов». Мы специально сделали фокус на утилитарном инжиниринге и production-ready решениях.

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

Зафиксировать цену и начать деплоить агентов без слива бюджета 👈

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

🕸 Пять типичных ошибок при проектировании интеграции с помощью Kafka

Система работала нормально, пока обработка батча не заняла слишком много времени. Координатор консьюмер-групп решил, что консьюмер мёртв, отверг коммит оффсета — и консьюмер начал читать сообщения заново. По кругу.

Это называется ложная ребалансировка, и это один из самых неочевидных сценариев поломки.

Архитектор Альфа-Банка разобрал пять таких историй из реального прода — с объяснением механики и тем, как этого избежать.

➡️ Читать статью

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

🔎 Удалённое комбо вакансий для шарпистов

Подобрали три вакансии для тех, кто не хочет выходить из дома:

.NET-разработчик

C#/.NET-разработчик

Senior Backend .NET Engineer

➡️ Еще больше топовых вакансий — в нашем канале C# Jobs

🐸 Библиотека шарписта

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

👨‍💻 Данные важнее алгоритмов

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

Почему структура данных первична

Когда данные организованы правильно, алгоритм становится очевидным. Его не нужно изобретать, ведь он вытекает из формы данных сам. Роб Пайк сформулировал это ещё в 1989 году, и с тех пор ничего не изменилось.

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

Что происходит, когда структура выбрана плохо

Состояние расползается по десяткам мест. Объекты мутируют там, где не должны. Каждое изменение ломает что-то в трёх других местах. Команда тратит время не на новые фичи, а на поиск причин, почему старые перестали работать.
Умный алгоритм, написанный поверх плохо организованных данных, не решает проблему. Он только откладывает её.

Что меняется после переосмысления структуры

Когда команда задаёт правильный вопрос: не "как это починить?", а "как данные должны течь через систему?", запутанный код превращается в понятный пайплайн. Становится короче. Баги исчезают. Новые фичи добавляются без страха сломать существующее.
Никакой магии. Просто правильно выбранная структура снимает нагрузку с алгоритма.

Практический ориентир

Перед тем как писать функцию, полезно ответить на четыре вопроса. Кто владеет этими данными. Где находится источник правды. Как данные попадают из точки А в точку Б. Соответствует ли структура тому, как к ней будут обращаться.
Если ответы нечёткие, сложность будет нарастать независимо от качества кода.

💬 Что думаете? Прав ли Роб Пайк или всё уже поменялось?

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#entry_point

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

✏️ Перегрузка и переопределение

На технических интервью по C# для джунов и иногда для мидлов вопрос про перегрузки и переопределения задаётся одним из первых.

Оба механизма связаны с методами и их именами. Оба выглядят похоже на первый взгляд. Но работают они в совершенно разных ситуациях и решают разные задачи.

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

➡️ Как ответить на собесе

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#dotnet_challenge

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

👨‍💻 Баги, которые уничтожат ваших пользователей

Большинство проблем с многопоточностью выглядят одинаково везде. Но часть из них появляется только в конкретной среде. 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


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

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

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

✌🏻 У нас две новости — хорошая и плохая!

Хорошая: Ваших знаний, скорее всего, хватит, чтобы собрать рабочую демку AI-агента в Colab. 🫡

Плохая: Вы вряд ли выведете его в прод, не обанкротившись на токенах и не слив базу. 🤯

Для защиты от таких сценариев мы полностью пересобрали курс «Разработка AI-агентов». Теперь внутри плотная работа с экономикой ресурсов, дебаг через time-travel в LangGraph, извлечение данных из кривых сканов для RAG и комплаенс по 152-ФЗ.

Если всё ещё сомневаетесь, послушайте голосовое от спикера курса Влада Прошинского, где он объясняет, как правильно тестировать агентов перед релизом.


Программа курса, полный состав спикеров и другие подробности 👈🏻

ВАЖНО! До 5 апреля на курс действует скидка, но свободные места могут закончиться раньше.

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

📎 Task.Run внутри ASP.NET пайплайна

Один из самых распространённых антипаттернов в .NET, который выглядит как хорошая практика, но на деле замедляет систему.

Обычный код:

await Task.Run(() => _logger.LogInformation("Processing..."));
await Task.Run(() => MapToDto(entity));
await Task.Run(() => ValidateHeaders(request));


Выглядит современно и async везде.

Что происходит на самом деле

Каждый Task.Run внутри ASP.NET запроса:

— ставит задачу в очередь thread pool
— вызывает context switch
— добавляет scheduling overhead

При этом ASP.NET уже работает на оптимально управляемом thread pool. Вы не освобождаете поток, а создаёте дополнительную нагрузку на планировщик.

Как надо:
// Логирование — всегда синхронно
_logger.LogInformation("Processing...");

// Маппинг — синхронно
var dto = MapToDto(entity);

// Валидация заголовков — синхронно
ValidateHeaders(request);

// async оставляем только для реального I/O
var data = await _repository.GetAsync(id);
var response = await _httpClient.GetAsync(url);


Есть ситуация, где он оправдан: долгая CPU-bound работа, которую нужно вынести за пределы потока запроса, чтобы не блокировать пайплайн.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

⚡️ Никаких больше var

Microsoft официально объявила: в C# 15 ключевое слово var признаётся устаревшим.

Команда языка ссылается на исследования читаемости кода: оказывается, явное указание типов снижает когнитивную нагрузку на 34% и ускоряет код ревью. Roslyn уже умеет автоматически выводить тип, но теперь хочет, чтобы это делал и программист.

Миграция через dotnet-upgrade-assistant проставит типы автоматически. Но 40 000 строк кода всё равно ждут вас в ближайшем будущем.

➡️ Источник

Попались? С первым апреля!😁

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

🚫 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);
}


Что происходит под капотом

Компилятор превращает async-метод примерно в это:

// Упрощённо — что генерирует компилятор
private struct ProcessAsyncStateMachine : IAsyncStateMachine
{
public byte[] data;
public Span<byte> span; // ← невозможно: ref struct не может быть полем
public int _state;
// ...
}


Стек фрейм между await не гарантирован, потому что поток может смениться, метод может возобновиться на другом потоке. Span на стеке к тому моменту уже не существует.

Как работать с данными в async-коде

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 только там, где он нужен непосредственно для вычислений.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

🎶 Разработчик написал программу для управления самодельным проигрывателем винила

Разработчик с Reddit строит автоматический проигрыватель пластинок с нуля: механику, электронику и прошивку для STM32. Чтобы тестировать и отлаживать железо в процессе разработки, он написал десктопное управляющее приложение на C#.

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

Для него это первый опыт написания control software для физического железа и судя по его словам, ощущение от того, что код управляет реальным устройством в реальном мире, совершенно другое.

➡️ Источник

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#entry_point

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

👨‍💻 Разделяемое состояние в многопоточке

Кажется, что примитивы атомарны. Это не так в смысле видимости между потоками: процессор и компилятор переупорядочивают инструкции, каждое ядро держит своё значение в кэше:

private bool _cacheLoaded;

// Поток A
_cacheLoaded = true;

// Поток B — может прочитать false, даже если A уже записал true
if (!_cacheLoaded) LoadCache(); // загружается дважды, данные затираются


Как это исправить

1. lock. Подходит для составных операций: «прочитать → изменить → записать» должны выполняться как одно целое:
private readonly object _sync = new object();
private int _count;

public void Increment()
{
lock (_sync) { _count++; }
}


2. volatile. Запрещает кэширование значения в регистре. Не заменяет lock. Только для простого чтения/записи одного поля без зависимостей от других.
private volatile bool _cacheLoaded;


3. Interlocked. Атомарная операция на уровне процессора. Быстрее lock, но только для простых числовых операций:
private int _count;

public void Increment()
{
Interlocked.Increment(ref _count);
}


📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

⚙️ 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 это кандидат на замену.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

⚙️ Покрытие кода для .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"


После выполнения в папке TestResults появится файл coverage.cobertura.xml с отчётом.

Второй вариант через MSBuild:
dotnet add package coverlet.msbuild
dotnet test /p:CollectCoverage=true


Итог сразу появится в терминале, а файл coverage.json сохранится в корне тестового проекта.

Третий вариант. Глобальный инструмент командной строки:
dotnet tool install --global coverlet.console
coverlet /path/to/test-assembly.dll --target "dotnet" --targetargs "test /path/to/test-project --no-build"


Четвёртый, самый новый, это интеграция с Microsoft Testing Platform. Подходит для проектов на Microsoft.Testing.Platform, требует .NET 8 и выше:
dotnet add package coverlet.MTP
dotnet test --coverlet


Если вы работаете в Visual Studio на Windows, расширение Fine Code Coverage умеет читать вывод Coverlet и подсвечивать покрытие прямо в редакторе. На macOS есть аналог VSMac-CodeCoverage.

Coverlet не требует сложной настройки, встраивается в стандартный dotnet test, работает на всех платформах и бесплатен.

➡️ Репозиторий

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#garbage_collector

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

🤔 Запросы в БД быстрые, кэш есть, 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

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

⚙️ 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 лучше и код понятнее. Но не стоит считать его нулевым по стоимости. Если участок горячий, то сначала замерьте, потом решайте.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#il_люминатор

Читать полностью…

Библиотека шарписта | C#, F#, .NET, ASP.NET

👨‍💻 Конфигурация везде это проблема

Когда конфигурация разбросана по всему коду, проект становится хрупким. Строки вроде _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 выбрасывает исключение при запуске, если конфигурация невалидна. Не в рантайме, не при первом запросе — сразу при старте.

Чистое внедрение через extension method

Чтобы не засорять 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;
}
}


В итоге конфигурация живёт в одном месте и имеет чёткую структуру. Все ключи типизированы и никаких магических строк.

📍 Навигация: ВакансииЗадачиСобесы

🐸 Библиотека шарписта

#sharp_view

Читать полностью…
Subscribe to a channel