csharp_ready | Unsorted

Telegram-канал csharp_ready - C# Ready | Unity

9818

Авторский канал по разработке на C# и Unity. Ресурсы, гайды, задачи, шпаргалки. Информация ежедневно пополняется! Автор: @energy_it РКН: https://clck.ru/3SBaT3 Реклама на бирже: https://telega.in/c/csharp_ready

Subscribe to a channel

C# Ready | Unity

CSharpGuide по C# и .NET!

Каталог разбит на шесть этапов, основы языка, управление потоком, методы и коллекции, ООП, продвинутый C# и практический .NET. Внутри этапов собраны отдельные уроки по переменным, LINQ, async/await, JSON, тестированию и другим темам.

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

Оставляю ссылочку на CSharpGuide


➡️ C# Ready | #ресурс

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

C# Ready | Unity

Gemini Pro на 24 месяца за 800₽!

Вместо 39.000₽ — всего 800₽ ($9.9) за 2 года. По акции сейчас это 33₽ в месяц, в 48 раз выгоднее!

➡Argon 4.0 + Claude Opus 5.5 — для кода
➡Nano Banana 2 Pro — для картинок
➡Генерация видео — ролики и анимации
➡AntiGravity + 5 ТБ облака
➡Opus 5.5 и Sonnet 5.5
➡Youtube Premium

Акция ограничена по времени, дальше будет официальная цена 480$!


Приобрести можно в Магазине с 20.000+ покупок и 2000+ отзывов (есть возвраты и замены).

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

C# Ready | Unity

Строим ограниченную очередь фоновых задач через Channel<T>!

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

Создаём очередь на 32 задания:

var queue = Channel.CreateBounded<Job>(
    new BoundedChannelOptions(32)
    { FullMode = BoundedChannelFullMode.Wait });


Режим Wait не отбрасывает новые элементы при заполнении. Вместо этого WriteAsync приостанавливает производителя до появления свободного слота.

Передаём задания с поддержкой отмены:
await queue.Writer.WriteAsync(job, token);
await queue.Writer.WriteAsync(nextJob, token);


Токен позволяет отменить ожидание записи, если приложение завершает операцию. В реальном producer-коде задания обычно записываются в цикле, а ошибки производства обрабатываются отдельно.

Потребитель читает элементы по мере появления:
await foreach (var job in queue.Reader
    .ReadAllAsync(token))
{
    await HandleAsync(job, token);
}


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

Для нескольких производителей можно завершить запись после их окончания:
try { await ProduceAsync(queue.Writer, token); }
finally { queue.Writer.TryComplete(); }


TryComplete закрывает очередь для новых записей, но позволяет обработать уже добавленные задания. Если producer завершился с исключением, его можно передать в TryComplete(error), чтобы consumer увидел причину остановки.

Запускаем производство и чтение вместе:
var consumer = ConsumeAsync(queue.Reader, token);
Exception? error = null;
try { await ProduceAsync(queue.Writer, token); }
catch (Exception ex) { error = ex; }
queue.Writer.TryComplete(error);
await consumer;


Ёмкость 32 ограничивает только число ожидающих элементов, но не время обработки.

➡️ C# Ready | #практика

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

C# Ready | Unity

Почему Task.Run не делает синхронный ввод-вывод асинхронным?

Оборачивать блокирующее чтение файла в Task.Run кажется способом освободить текущий поток, но ожидание всё равно занимает поток из пула:

await Task.Run(() => File.ReadAllText(path));


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

Для чтения файла используем API с настоящей асинхронной операцией:
string text = await File.ReadAllTextAsync(
    path, cancellationToken);


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

Токен отмены передаём и в сам метод чтения, и дальше по цепочке:
await using var stream = File.OpenRead(path);
using var reader = new StreamReader(stream);
string text = await reader.ReadToEndAsync(token);


Для CPU-вычислений Task.Run может быть уместен, например чтобы не блокировать UI-поток:
var result = await Task.Run(
    () => Calculate(input, token), token);


Синхронная функция должна сама проверять отмену, потому что токен не прерывает уже запущенный код:
foreach (var item in items)
{
    token.ThrowIfCancellationRequested();
    Process(item);
}


Независимые операции с асинхронным API запускаем напрямую и ожидаем вместе:
Task<string> config = File.ReadAllTextAsync(
    configPath);
Task<string> data = File.ReadAllTextAsync(
    dataPath);
await Task.WhenAll(config, data);


Так ожидание I/O не расходует потоки на блокировку. В серверном коде Task.Run вокруг обычного ввода-вывода чаще добавляет переключение потоков, а не пропускную способность.

➡️ C# Ready | #совет

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

C# Ready | Unity

Знаете, почему регулярное выражение может надолго занять поток?

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

var pattern = "^(a+)+$";
var regex = new Regex(pattern);


По умолчанию Regex может работать без ограничения времени. Для обычного движка задаём конечный timeout при создании объекта:
var regex = new Regex(pattern, RegexOptions.None,
    TimeSpan.FromMilliseconds(100));


Если поиск не уложится в интервал, будет выброшено RegexMatchTimeoutException. Обрабатываем её отдельно и возвращаем понятную ошибку проверки:
try {
    return regex.IsMatch(input);
} catch (RegexMatchTimeoutException) {
    throw new ValidationException(
        "Regex validation timed out");
}


В .NET 7 и новее можно рассмотреть RegexOptions.NonBacktracking, если шаблон не использует неподдерживаемые конструкции вроде lookaround или backreference.

➡️ C# Ready | #совет

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

C# Ready | Unity

Запускаем периодическую проверку без наложения запросов!

Обычный таймер может вызвать следующий callback, пока предыдущая проверка ещё ждёт сеть. Сегодня соберём последовательный polling через PeriodicTimer и передадим отмену внутрь операции.

Создаём таймер с нужным интервалом:

using var timer = new PeriodicTimer(
    TimeSpan.FromSeconds(15));


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

Ждём тик и завершаем цикл через cancellation token:
while (await timer.WaitForNextTickAsync(token))
{
    await RefreshAsync(token);
}


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

Внутри проверки передаём тот же token сетевому вызову:
using var response = await client.GetAsync(
    endpoint, token);
response.EnsureSuccessStatusCode();


При остановке фоновой службы token прервёт ожидание тика и поддерживающий отмену HTTP-запрос.

➡️ C# Ready | #практика

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

C# Ready | Unity

Знали, почему долгоживущий HttpClient может обращаться по устаревшему DNS-адресу?

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

Создаём общий клиент и задаём срок жизни соединений:

static readonly HttpClient Client = new(
    new SocketsHttpHandler
    {
        PooledConnectionLifetime =
            TimeSpan.FromMinutes(5)
    });


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

Клиент повторно используем между запросами:
using var response =
    await Client.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();


Создание нового HttpClient на каждый запрос может постоянно плодить пулы соединений и привести к исчерпанию сокетов. Для приложений с DI вместо статического клиента подходит IHttpClientFactory с настройкой handler lifetime.

➡️ C# Ready | #совет

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

C# Ready | Unity

Проверяем заголовок PNG и читаем размеры!

Нужно быстро узнать размеры изображения до декодирования всего файла. В этой задаче читаем фиксированные 24 байта, проверяем сигнатуру PNG и первый чанк IHDR, затем получаем ширину и высоту.

В решении:

• читаем заголовок без загрузки изображения целиком
• отклоняем усечённый или неверный файл
• декодируем размеры с учётом порядка байтов


Такой код подходит для предварительной проверки загрузок и очередей обработки изображений.

➡️ C# Ready | #задача

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

C# Ready | Unity

Зачем передавать TimeProvider вместо прямого вызова DateTimeOffset.UtcNow?

Если код сам читает системные часы, тест зависит от момента запуска. Передай источник времени явно:

public sealed class ExpirationService(TimeProvider clock)
{
    public bool IsExpired(DateTimeOffset expiresAt) =>
        clock.GetUtcNow() >= expiresAt;
}


Сервис теперь не привязан к часам машины. В приложении используй системный провайдер:
services.AddSingleton<TimeProvider>(TimeProvider.System);


В тесте установи фиксированное время через FakeTimeProvider:
var clock = new FakeTimeProvider(
    new DateTimeOffset(2030, 1, 1, 12, 0, 0, TimeSpan.Zero));
var service = new ExpirationService(clock);


Создай дату истечения и сдвинь виртуальные часы до границы:
var expiresAt = clock.GetUtcNow().AddMinutes(5);
clock.Advance(TimeSpan.FromMinutes(5));
Assert.True(service.IsExpired(expiresAt));


Тест выполняется сразу и точно проверяет границу. Добавь пакет для управляемых часов:
dotnet add package Microsoft.Extensions.TimeProvider.Testing


Для управляемых задержек передай тот же провайдер в Task.Delay:
await Task.Delay(TimeSpan.FromMinutes(5), clock, cancellationToken);


В тесте Advance завершит такое ожидание без реальной паузы. Для измерения скорости используй Stopwatch, а не календарные часы.

➡️ C# Ready | #совет

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

C# Ready | Unity

Добавляем Request-Id middleware в ASP.NET Core!

Нужно связывать логи, ошибки и ответы API одним идентификатором запроса. Реализуем middleware, который берёт Request-Id из заголовка или создаёт новый.

В этой задаче:

• читаем входящий заголовок
• создаём fallback через Guid
• прокидываем значение в response
• подключаем middleware в pipeline


Такой подход помогает быстрее искать конкретный запрос в логах, трассировке, gateway и клиентских жалобах.

➡️ C# Ready | #задача

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

C# Ready | Unity

Разбираем 7 инструментов System.Text.Json!

С JSON можно работать как с типизированными объектами, так и без заранее описанной модели. Для потоков есть асинхронные методы, а для выборочного чтения и изменения структуры подходят разные представления документа.

В этой шпоре собраны Serialize, Deserialize, SerializeAsync, DeserializeAsync, JsonDocument, TryGetProperty и JsonNode.

➡️ C# Ready | #шпора

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

C# Ready | Unity

Карта основных возможностей C#!

На картинке язык разложен на крупные ветки: типы, операторы, инструкции, классы, коллекции и LINQ. Цвета помогают быстро увидеть, какие темы относятся к одной группе.

Например, ветка типов ведёт от числовых и ссылочных типов к пользовательским классам, а ветка LINQ объединяет фильтрацию, сортировку, группировку и преобразование данных.

Сохрани, чтобы не потерять!

➡️ C# Ready | #ресурс

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

C# Ready | Unity

Разбираем 7 приёмов для регулярных выражений в C#!

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

➡️ C# Ready | #шпора

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

C# Ready | Unity

Почему для денег в C# обычно нужен decimal, а не double?

double удобен для инженерных расчётов, координат, графики и статистики. Но для денег он часто создаёт ошибки.

Причина в том, что double хранит число в двоичном формате с плавающей точкой. Не все десятичные дроби можно представить точно.

Простой пример выглядит так:

double price = 0.1 + 0.2;
Console.WriteLine(price);


Результат может оказаться не ровно 0.3, а числом с маленьким хвостом. Для UI это иногда скрывается форматированием, но в сравнении и расчётах ошибка остаётся.

Особенно плохо сравнивать такие значения напрямую:
if (price == 0.3) {
Console.WriteLine("ok");
}


В финансовых сценариях обычно используют decimal:
decimal price = 0.1m + 0.2m;
Console.WriteLine(price);


Суффикс m важен. Без него литерал сначала будет double, и смысл decimal частично потеряется.

Для округления лучше явно указать правило:
decimal total = Math.Round(price, 2, MidpointRounding.AwayFromZero);


Так код показывает, как именно округляются копейки, налоги, скидки или комиссии.

decimal медленнее и занимает больше памяти, чем double. Но для денег обычно важнее предсказуемость, чем скорость.

➡️ C# Ready | #совет

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

C# Ready | Unity

Делаем простой feature flags на C#!

Feature flags позволяют включать и выключать поведение без переписывания кода. Это удобно для новых экранов и постепенного выката функций.

Для примера будем хранить флаги в словаре:

var flags = new Dictionary<string, bool>
{
["new-checkout"] = true,
["debug-panel"] = false
};


Сделаем маленькую функцию проверки:
static bool IsEnabled(
Dictionary<string, bool> flags,
string name)


Внутри используем TryGetValue, чтобы не падать на неизвестном флаге:
return flags.TryGetValue(name, out var enabled)
&& enabled;


Теперь флаг можно использовать в коде:
if (IsEnabled(flags, "new-checkout"))
{
ShowNewCheckout();
}


Если флага нет, функция вернёт false:
Console.WriteLine(
IsEnabled(flags, "unknown"));


В реальном проекте словарь можно заполнять из JSON, базы или remote config. Главное, чтобы код спрашивал флаг через один понятный метод.

➡️ C# Ready | #практика

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

C# Ready | Unity

Почему ValueTask не стоит использовать ради ускорения?

ValueTask часто воспринимают как более быстрый Task. На практике он полезен не всегда и может сделать код сложнее.

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

Такой метод может вернуть готовое значение без лишней аллокации:

if (cache.TryGetValue(id, out var user))
{
return new ValueTask(user);
}


А если значения нет, можно перейти к обычной async-операции:
return new ValueTask(LoadAsync(id));


Но у ValueTask есть ограничения. Его нельзя бездумно ждать несколько раз, сохранять на потом или передавать по цепочке как обычный Task.

Плохой пример выглядит так:
var result = GetUserAsync(id);
await result;
await result;


С Task такое обычно безопасно, а с ValueTask можно получить неожиданное поведение, особенно если внутри используется IValueTaskSource.

Если метод почти всегда делает настоящий async I/O, лучше оставить Task:
Task LoadUserAsync(int id)
{
return db.Users.FindAsync(id).AsTask();
}


ValueTask используйте только там, где на то есть причина и польза.

➡️ C# Ready | #совет

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

C# Ready | Unity

Шпаргалка по class, record и struct в C#!

Например, class и record class являются ссылочными типами, а struct и record struct передают значения как отдельные данные.

На карточках сравниваются равенство, присваивание и копирование через with. Отдельно показано, почему with не создаёт глубокую копию вложенного списка.

Сохрани, чтобы не потерять!

➡️ C# Ready | #шпора

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

C# Ready | Unity

Строим порядок сборки из зависимостей!

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

В этой задаче:

• строим граф зависимостей и считаем входящие связи
• обрабатываем готовые задачи через очередь
• проверяем, что в графе не осталось цикла


Алгоритм Кана проходит каждую задачу и каждую связь один раз.

➡️ C# Ready | #задача

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

C# Ready | Unity

Добавляем замер времени HTTP-запросов через DelegatingHandler!

Когда несколько сервисов обращаются к внешним API, можно видеть статус и длительность каждого запроса. Вынесем замер в обработчик HttpClient, чтобы не дублировать Stopwatch в каждом месте вызова.

Создаём обработчик, который получает логгер через конструктор:

sealed class TimingHandler(
    ILogger<TimingHandler> log) :
    DelegatingHandler
{
}


DelegatingHandler передаёт запрос следующему обработчику цепочки. Параметр primary constructor доступен методам класса, поэтому отдельное поле для log не требуется.

Переопределяем асинхронную отправку и сохраняем контракт базового класса:
protected override
    Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken token)


Токен отмены передаём дальше без замены, чтобы отмена вызывающего кода дошла до сетевого запроса.

Перед отправкой запускаем секундомер и готовим поле статуса:
var timer = Stopwatch.StartNew();
HttpStatusCode? status = null;


Переменная status остаётся null, если транспорт завершился исключением до получения ответа. Это позволяет логировать и успешные ответы, и неудачные попытки соединения.

Выполняем запрос через следующий обработчик и запоминаем статус:
try
{
    var response = await base.SendAsync(
        request, token);
    status = response.StatusCode;
    return response;
}


Ответ возвращается вызывающему коду без чтения или изменения его тела. Блок finally выполняется и после ответа, и при сетевом исключении.

В finally записываем метод, статус и длительность:
finally
{
    log.LogInformation(
        "{Method} {Status} {Ms}ms",
        request.Method, status,
        timer.ElapsedMilliseconds);
}


Регистрируем обработчик как transient и добавляем его в именованный клиент:
builder.Services.AddTransient<TimingHandler>();
builder.Services.AddHttpClient("catalog")
    .AddHttpMessageHandler<TimingHandler>();


Фабрика создаёт цепочки обработчиков для клиентов, поэтому один экземпляр DelegatingHandler нельзя переиспользовать в нескольких цепочках.

➡️ C# Ready | #практика

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

C# Ready | Unity

Как код C# превращается в машинные инструкции!

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

При запуске CLR загружает сборку. Когда вызывается метод, JIT-компилятор переводит его IL в машинный код для текущей платформы.

Сохрани, чтобы не потерять!

➡️ C# Ready | #ресурс

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

C# Ready | Unity

Как индексация строк изменила систему типов HydraScript!

Автор добавил длину и обращение по индексу в интерпретируемый язык на C#. Работа над строками показала, что правила операций удобнее хранить рядом с типами, а не разносить по семантическому анализатору.

В статье разбирают:
• как интерфейс IOperator описывает допустимость операции и тип результата
• почему строки и массивы могут использовать общие правила индексации
• где проходит граница между проверкой типов и выполнением выражения

Такой подход упрощает развитие анализатора, но оставляет диагностику ошибок и контекст исходного кода в соответствующем слое.

Продолжай читать на Habr!


➡️ C# Ready | #статья

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

C# Ready | Unity

Шпаргалка по сериализации JSON в C#!

Source generation создаёт код сериализации во время сборки и помогает проектам с trimming и Native AOT. В контекст добавляются используемые модели, а вызовы получают сгенерированный JsonTypeInfo без поиска метаданных через reflection.

➡️ C# Ready | #шпора

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

C# Ready | Unity

Обрабатываем список запросов параллельно с ограничением!

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

Сначала задаём лимит параллелизма и общий токен отмены:

var options = new ParallelOptions
{
    MaxDegreeOfParallelism = 4,
    CancellationToken = cancellationToken
};


Число четыре ограничивает одновременно выполняемые обработчики, а не размер всего входного списка. Подбирай значение по лимитам API, пулу соединений и измерениям, а не только по числу ядер процессора.

Передаём каждую запись в асинхронную операцию и не блокируем поток через Wait или Result:
await Parallel.ForEachAsync(records, options, async (record, token) =>
{
    await client.PostAsJsonAsync("/events", record, token);
});


Внутренний token связан с ParallelOptions. Передавай его дальше в HTTP-вызов, чтобы отмена работала не только между элементами, но и во время сетевого ожидания.

Соберём результат ответа и явно обработаем неуспешный HTTP-статус:
await Parallel.ForEachAsync(records, options, async (record, token) =>
{
    using var response = await client.PostAsJsonAsync(
        "/events", record, token);
    response.EnsureSuccessStatusCode();
});


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

Запусти обработку внутри обычного try и отличай отмену от сбоя сервера:
try
{
    await Parallel.ForEachAsync(records, options, ProcessAsync);
}


Ожидаемую отмену обработай отдельно от ошибок сервера:
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
    logger.LogInformation("Обработка отменена");
}


Сетевые ошибки можно записать отдельно или передать вызывающему уровню:
catch (HttpRequestException exception)
{
    logger.LogError(exception, "Не удалось отправить запись");
}


Parallel.ForEachAsync подходит для независимых элементов, когда важен ограниченный параллелизм. Лимит не задаёт частоту запросов в секунду, не сохраняет порядок завершения и сам по себе не реализует повторы.

➡️ C# Ready | #практика

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

C# Ready | Unity

Шпаргалка по регулярным выражениям в .NET!

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

На картинке есть IsMatch, Match, Matches, Replace, Split, Count, EnumerateMatches, Escape и Unescape. Также показаны RegexOptions и тайм-аут, который ограничивает время поиска при неудачном шаблоне.

➡️ C# Ready | #ресурс

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

C# Ready | Unity

Убираем лишние запросы при вводе текста в C#!

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

Храним источник отмены текущего поиска и блокировку для его безопасной замены:

private readonly object _searchLock = new();
private CancellationTokenSource? _searchCts;


Когда приходит новый текст, заменяем источник под блокировкой и отменяем предыдущий поиск:
var current = new CancellationTokenSource();
lock (_searchLock)
{
    var previous = _searchCts;
    _searchCts = current;
    previous?.Cancel();
}


Блокировка не даёт старой задаче одновременно освободить источник, который новый вызов пытается отменить. Каждый вызов освобождает свой объект сам после завершения.

Ждём короткую паузу, затем выполняем поиск с тем же токеном отмены:
try
{
    await Task.Delay(300, current.Token);
    var results = await _searchService.FindAsync(query, current.Token);
    current.Token.ThrowIfCancellationRequested();
    _view.Show(results);
}
catch (OperationCanceledException) when (current.IsCancellationRequested)
{
}


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

В блоке finally очищаем поле только тогда, когда там всё ещё наш источник, и освобождаем его:
finally
{
    lock (_searchLock)
    {
        if (ReferenceEquals(_searchCts, current))
        {
            _searchCts = null;
        }
    }
    current.Dispose();
}


Такой debounce сокращает сетевую нагрузку.

➡️ C# Ready | #практика

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

C# Ready | Unity

Знали, как читать обновляемые настройки из singleton-сервиса в ASP.NET Core?

Фоновый сервис обычно живёт всё время работы приложения. Если внедрить в него IOptionsSnapshot<T>, контейнер зависимостей получит scoped-сервис внутри singleton, и такая комбинация нарушает границы времени жизни.

Для долгоживущих сервисов предназначен IOptionsMonitor<T>. Он зарегистрирован как singleton, подходит для внедрения в сервисы любого времени жизни и позволяет получить актуальные настройки через CurrentValue.

Сначала связываем класс настроек с секцией конфигурации:

builder.Services.Configure<WorkerOptions>(
    builder.Configuration.GetSection("Worker"));


В singleton сохраняем сам монитор, а не снимок настроек, прочитанный при создании сервиса:
private readonly IOptionsMonitor<WorkerOptions> _options;


Перед каждой операцией получаем актуальное значение и используем его как единый набор параметров для этой операции:
WorkerOptions current = _options.CurrentValue;
await RunJobAsync(current.BatchSize, current.Endpoint);


Если нужно реагировать на изменение сразу, монитор поддерживает уведомления OnChange. Подписку создают один раз на время жизни сервиса и освобождают при остановке, а не создают заново при каждом запросе.

➡️ C# Ready | #совет

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

C# Ready | Unity

Почему MemoryStream.GetBuffer может вернуть лишние байты?

Представим, что API должен отправить короткий двоичный ответ. Мы записали его в MemoryStream, а затем получили внутренний массив:

using var stream = new MemoryStream();
stream.Write(new byte[] { 1, 2, 3 });
byte[] data = stream.GetBuffer();


Кажется, что data должен содержать ровно три байта. Но MemoryStream часто выделяет буфер с запасом. Длина массива отражает вместимость буфера, а не количество записанных данных.

Эти числа можно сравнить напрямую:
Console.WriteLine(stream.Length);
Console.WriteLine(stream.Capacity);
Console.WriteLine(data.Length);


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

Когда нужен самостоятельный массив точной длины, используй ToArray:
byte[] payload = stream.ToArray();
await destination.WriteAsync(payload);


ToArray копирует только записанные данные. Копия требует дополнительной памяти, зато её длина соответствует содержимому и она не изменится при последующей записи в stream.

Если копирование дорого, можно получить сегмент внутреннего массива вместе с правильными Offset и Count:
if (stream.TryGetBuffer(out ArraySegment<byte> segment)) {
await destination.WriteAsync(segment.AsMemory());
}


Не каждый MemoryStream разрешает доступ к своему буферу, GetBuffer может выбросить исключение, а TryGetBuffer в этом случае вернёт false.

➡️ C# Ready | #совет

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

C# Ready | Unity

Планируем ближайшие задачи через PriorityQueue в C#!

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

Создадим очередь, где приоритетом будет время запуска:

var jobs = new PriorityQueue<string, DateTimeOffset>();
var now = DateTimeOffset.UtcNow;
jobs.Enqueue("backup", now.AddMinutes(20));
jobs.Enqueue("cleanup", now.AddMinutes(5));


Внутри может быть много заданий, но для демонстрации добавлены два. cleanup получит меньший приоритет, потому что его время ближе.

Перед обработкой проверим, наступил ли срок ближайшего задания:
if (jobs.TryPeek(out _, out var due) && due <= now)
    jobs.TryDequeue(out var job, out _);


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

Когда очередь проверяется регулярно, извлекай задания циклом, чтобы обработать все уже просроченные:
while (jobs.TryPeek(out _, out var due) && due <= DateTimeOffset.UtcNow) {
    var job = jobs.Dequeue();
    Console.WriteLine(job);
}


Для параллельной обработки можно передавать извлечённые элементы в ограниченную очередь работников.

➡️ C# Ready | #практика

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

C# Ready | Unity

Разбираем System.Threading.Channels в C#!

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

В этой шпоре собраны CreateUnbounded, CreateBounded, Writer, Reader, WriteAsync, ReadAllAsync и Complete.

➡️ C# Ready | #шпора

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

C# Ready | Unity

Читаем большой JSON-массив по одному объекту в C#!

Пусть orders.json содержит тысячи заказов одним массивом. Если загрузить файл целиком через ReadAllText, память займут и исходный текст, и созданные объекты. Для последовательной обработки удобнее читать поток и получать элементы по мере разбора.

Сначала опишем модель входных данных. В примере у каждого заказа есть номер и сумма:

public sealed record Order(int Id, decimal Total);


Откроем файл как поток. JsonSerializer.DeserializeAsyncEnumerable ожидает JSON-массив и возвращает асинхронную последовательность его элементов:
await using var stream = File.OpenRead("orders.json");
var orders = JsonSerializer.DeserializeAsyncEnumerable<Order>(
stream, cancellationToken: cancellationToken);


Здесь cancellationToken позволяет прервать длительную обработку, например при остановке запроса или фоновой задачи. Не надо самостоятельно резать JSON на строки, переносы строк не определяют границы объектов.

Обработаем элементы по одному и накопим итог. Нулевые элементы пропустим, а отрицательную сумму считаем ошибкой данных:
decimal total = 0;
await foreach (Order? order in orders) {
if (order is null) continue;
if (order.Total < 0) throw new InvalidDataException("Сумма меньше нуля");
total += order.Total;
}


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

➡️ C# Ready | #практика

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