Библиотека шарписта | C#, F#, .NET, ASP.NET
Все самое полезное для C#-разработчика в одном канале. По рекламе: @proglib_adv Учиться у нас: https://proglib.io/w/b60af5a4 Для обратной связи: @proglibrary_feeedback_bot РКН: https://gosuslugi.ru/snet/67a5c81cdc130259d5b7fead
Show more📈 Analytical overview of Telegram channel Библиотека шарписта | C#, F#, .NET, ASP.NET
Channel Библиотека шарписта | C#, F#, .NET, ASP.NET (@csharpproglib) in the Russian language segment is an active participant. Currently, the community unites 21 872 subscribers, ranking 6 212 in the Technologies & Applications category and 30 851 in the Russia region.
📊 Audience metrics and dynamics
Since its creation on невідомо, the project has demonstrated rapid growth, gathering an audience of 21 872 subscribers.
According to the latest data from 10 June, 2026, the channel demonstrates stable activity. Although there has been a change in the number of participants by -87 over the last 30 days and by -4 over the last 24 hours, overall reach remains high.
- Verification status: Not verified
- Engagement rate (ER): The average audience engagement rate is 12.06%. Within the first 24 hours after publication, content typically collects 7.04% reactions from the total number of subscribers.
- Post reach: On average, each post receives 2 638 views. Within the first day, a publication typically gains 1 540 views.
- Reactions and interaction: The audience actively supports content: the average number of reactions per post is 8.
- Thematic interests: Content is focused on key topics such as .net, шарписта, навигация, await, string.
📝 Description and content policy
The author describes the resource as a platform for expressing subjective opinions:
“Все самое полезное для C#-разработчика в одном канале.
По рекламе: @proglib_adv
Учиться у нас: https://proglib.io/w/b60af5a4
Для обратной связи: @proglibrary_feeedback_bot
РКН: https://gosuslugi.ru/snet/67a5c81cdc130259d5b7fead”
Thanks to the high frequency of updates (latest data received on 11 June, 2026), the channel maintains relevance and a high level of publication reach. Analytics show that the audience actively interacts with content, making it an important point of influence in the Technologies & Applications category.
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_люминаторLangGraph, извлечение данных из кривых сканов для RAG и комплаенс по 152-ФЗ.
Если всё ещё сомневаетесь, послушайте голосовое от спикера курса Влада Прошинского, где он объясняет, как правильно тестировать агентов перед релизом.Программа курса, полный состав спикеров и другие подробности 👈🏻 ВАЖНО! До 5 апреля на курс действует скидка, но свободные места могут закончиться раньше.
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_viewawait 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_viewdotnet-upgrade-assistant проставит типы автоматически. Но 40 000 строк кода всё равно ждут вас в ближайшем будущем.
➡️ Источник
Попались? С первым апреля!😁
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта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_viewSpan<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_люминатор// "Правильное" решение — 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_люминатор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 int _count;
public void Increment()
{
Interlocked.Increment(ref _count);
}
3. Interlocked. Атомарная операция на уровне процессора. Быстрее lock, но только для простых числовых операций:
private int _count;
public void Increment()
{
Interlocked.Increment(ref _count);
}
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#sharp_viewnew string(...). Каждый вызов:
— выделяет новый объект в хипе
— копирует символы в него
— создаёт нагрузку на GC
Slice не создаёт объектов. Это просто новый указатель + длина поверх той же памяти. int.Parse(ReadOnlySpan<char>) читает символы напрямую оттуда.
Частая ошибка
// Так делать не надо — убивает весь смысл
int id = int.Parse(span.Slice(5, 2).ToString());
ToString() на Span создаёт новую строку. Вернулись к исходной проблеме.
Одно правило: если Substring перед Parse это кандидат на замену.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#il_люминатор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_viewdotnet 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_viewMCP и A2A-взаимодействие, чтобы агенты могли вас читать;
— научиться контролировать стоимость (лимиты, кэш, роутинг между моделями);
— настроить AgentOps: трейсинг, логирование и отлов регрессий.
Всё это ждёт вас на обновлённом курсе «Разработка AI-агентов». Мы специально сделали фокус на утилитарном инжиниринге и production-ready решениях.
Кстати, до 29 марта можно забрать курс с большой скидкой, и стоит поторопиться — мест на потоке всё меньше.
Зафиксировать цену и начать деплоить агентов без слива бюджета 👈Substring(), Split() или ToArray(), то вы создаёте новые объекты на хипе. При большой нагрузке это тысячи объектов в секунду.
А дальше приходит Garbage Collector и делает паузу. Вот откуда те самые загадочные всплески задержек.
Что делать:
→ Используйте Span<T> и ReadOnlySpan<T> вместо substring
→ ArrayPool<T>.Shared.Rent() вместо new массивов
→ StringBuilder пулить через ObjectPool
→ Профилируйте через dotnet-trace + BenchmarkDotNet
Правило простое: меньше объектов на хипе → реже GC → стабильная латентность. Иногда пара изменений убирает 80% тормозов.
📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#sharp_view
Available now! Telegram Research 2025 — the year's key insights 
