Зачем передавать TimeProvider вместо прямого вызова DateTimeOffset.UtcNow?
Если код сам читает системные часы, тест зависит от момента запуска. Передай источник времени явно:
Сервис теперь не привязан к часам машины. В приложении используй системный провайдер:
В тесте установи фиксированное время через FakeTimeProvider:
Создай дату истечения и сдвинь виртуальные часы до границы:
Тест выполняется сразу и точно проверяет границу. Добавь пакет для управляемых часов:
Для управляемых задержек передай тот же провайдер в Task.Delay:
В тесте Advance завершит такое ожидание без реальной паузы. Для измерения скорости используй Stopwatch, а не календарные часы.
➡️ C# Ready | #совет
Если код сам читает системные часы, тест зависит от момента запуска. Передай источник времени явно:
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, а не календарные часы.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤5🔥4
Обрабатываем список запросов параллельно с ограничением!
Представь, что нужно отправить тысячи записей во внешний API. Последовательный цикл может работать слишком долго, а запуск тысячи Task сразу создаст всплеск соединений и нагрузки. Соберём обработчик, который ограничивает число одновременных операций и умеет штатно отменяться.
Сначала задаём лимит параллелизма и общий токен отмены:
Число четыре ограничивает одновременно выполняемые обработчики, а не размер всего входного списка. Подбирай значение по лимитам API, пулу соединений и измерениям, а не только по числу ядер процессора.
Передаём каждую запись в асинхронную операцию и не блокируем поток через Wait или Result:
Внутренний token связан с ParallelOptions. Передавай его дальше в HTTP-вызов, чтобы отмена работала не только между элементами, но и во время сетевого ожидания.
Соберём результат ответа и явно обработаем неуспешный HTTP-статус:
Освобождение ответа важно при массовой обработке. Если один запрос вернул ошибку, исключение выйдет из цикла, поэтому вызывающему коду нужно решить, завершать всю пачку, повторять отдельную запись или собирать ошибки поэлементно.
Запусти обработку внутри обычного try и отличай отмену от сбоя сервера:
Ожидаемую отмену обработай отдельно от ошибок сервера:
Сетевые ошибки можно записать отдельно или передать вызывающему уровню:
Parallel.ForEachAsync подходит для независимых элементов, когда важен ограниченный параллелизм. Лимит не задаёт частоту запросов в секунду, не сохраняет порядок завершения и сам по себе не реализует повторы.
➡️ C# Ready | #практика
Представь, что нужно отправить тысячи записей во внешний 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 подходит для независимых элементов, когда важен ограниченный параллелизм. Лимит не задаёт частоту запросов в секунду, не сохраняет порядок завершения и сам по себе не реализует повторы.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7👍3🔥3
Проверяем заголовок PNG и читаем размеры!
Нужно быстро узнать размеры изображения до декодирования всего файла. В этой задаче читаем фиксированные 24 байта, проверяем сигнатуру PNG и первый чанк IHDR, затем получаем ширину и высоту.
В решении:
Такой код подходит для предварительной проверки загрузок и очередей обработки изображений.
➡️ C# Ready | #задача
Нужно быстро узнать размеры изображения до декодирования всего файла. В этой задаче читаем фиксированные 24 байта, проверяем сигнатуру PNG и первый чанк IHDR, затем получаем ширину и высоту.
В решении:
• читаем заголовок без загрузки изображения целиком
• отклоняем усечённый или неверный файл
• декодируем размеры с учётом порядка байтов
Такой код подходит для предварительной проверки загрузок и очередей обработки изображений.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍7❤4🔥4
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7🔥5👍3🤝2
Знали, почему долгоживущий HttpClient может обращаться по устаревшему DNS-адресу?
HttpClient повторно использует соединения из пула, а DNS обычно проверяется при установке нового соединения. Если адрес сервиса изменился, уже открытое соединение не обязано сразу переключиться на новый IP.
Создаём общий клиент и задаём срок жизни соединений:
После истечения интервала старое соединение перестаёт использоваться для новых запросов, а следующее подключение заново разрешает имя хоста. Значение выбираем по ожидаемой частоте DNS-изменений и особенностям инфраструктуры.
Клиент повторно используем между запросами:
Создание нового HttpClient на каждый запрос может постоянно плодить пулы соединений и привести к исчерпанию сокетов. Для приложений с DI вместо статического клиента подходит IHttpClientFactory с настройкой handler lifetime.
➡️ C# Ready | #совет
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.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍3🔥3
Как индексация строк изменила систему типов HydraScript!
Автор добавил длину и обращение по индексу в интерпретируемый язык на C#. Работа над строками показала, что правила операций удобнее хранить рядом с типами, а не разносить по семантическому анализатору.
В статье разбирают:
• как интерфейс IOperator описывает допустимость операции и тип результата
• почему строки и массивы могут использовать общие правила индексации
• где проходит граница между проверкой типов и выполнением выражения
Такой подход упрощает развитие анализатора, но оставляет диагностику ошибок и контекст исходного кода в соответствующем слое.
➡️ C# Ready | #статья
Автор добавил длину и обращение по индексу в интерпретируемый язык на C#. Работа над строками показала, что правила операций удобнее хранить рядом с типами, а не разносить по семантическому анализатору.
В статье разбирают:
• как интерфейс IOperator описывает допустимость операции и тип результата
• почему строки и массивы могут использовать общие правила индексации
• где проходит граница между проверкой типов и выполнением выражения
Такой подход упрощает развитие анализатора, но оставляет диагностику ошибок и контекст исходного кода в соответствующем слое.
Продолжай читать на Habr!
Please open Telegram to view this post
VIEW IN TELEGRAM
❤5🔥4👍3
Запускаем периодическую проверку без наложения запросов!
Обычный таймер может вызвать следующий callback, пока предыдущая проверка ещё ждёт сеть. Сегодня соберём последовательный polling через PeriodicTimer и передадим отмену внутрь операции.
Создаём таймер с нужным интервалом:
PeriodicTimer допускает одного ожидающего потребителя. Если очередная проверка длится дольше интервала, накопившиеся тики не создают параллельные копии запроса.
Ждём тик и завершаем цикл через cancellation token:
Следующая итерация начнётся только после завершения RefreshAsync. Поэтому медленный ответ увеличит фактический интервал, но проверки не наложатся друг на друга.
Внутри проверки передаём тот же token сетевому вызову:
При остановке фоновой службы token прервёт ожидание тика и поддерживающий отмену HTTP-запрос.
➡️ C# Ready | #практика
Обычный таймер может вызвать следующий 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-запрос.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤8👍4🔥4
Как код C# превращается в машинные инструкции!
Схема показывает весь путь от исходного файла до выполнения процессором. Компилятор C# сначала создаёт промежуточный IL и сборку, а не готовые инструкции для конкретного процессора.
При запуске CLR загружает сборку. Когда вызывается метод, JIT-компилятор переводит его IL в машинный код для текущей платформы.
Сохрани, чтобы не потерять!
➡️ C# Ready | #ресурс
Схема показывает весь путь от исходного файла до выполнения процессором. Компилятор C# сначала создаёт промежуточный IL и сборку, а не готовые инструкции для конкретного процессора.
При запуске CLR загружает сборку. Когда вызывается метод, JIT-компилятор переводит его IL в машинный код для текущей платформы.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10❤4🔥3
Знаете, почему регулярное выражение может надолго занять поток?
При неудачном совпадении шаблоны с вложенными повторами могут проверять множество вариантов. На пользовательском вводе такая обратная проверка способна надолго загрузить процессор:
По умолчанию Regex может работать без ограничения времени. Для обычного движка задаём конечный timeout при создании объекта:
Если поиск не уложится в интервал, будет выброшено RegexMatchTimeoutException. Обрабатываем её отдельно и возвращаем понятную ошибку проверки:
В .NET 7 и новее можно рассмотреть RegexOptions.NonBacktracking, если шаблон не использует неподдерживаемые конструкции вроде lookaround или backreference.
➡️ C# Ready | #совет
При неудачном совпадении шаблоны с вложенными повторами могут проверять множество вариантов. На пользовательском вводе такая обратная проверка способна надолго загрузить процессор:
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.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8❤4🔥3
Добавляем замер времени HTTP-запросов через DelegatingHandler!
Когда несколько сервисов обращаются к внешним API, можно видеть статус и длительность каждого запроса. Вынесем замер в обработчик HttpClient, чтобы не дублировать Stopwatch в каждом месте вызова.
Создаём обработчик, который получает логгер через конструктор:
DelegatingHandler передаёт запрос следующему обработчику цепочки. Параметр primary constructor доступен методам класса, поэтому отдельное поле для log не требуется.
Переопределяем асинхронную отправку и сохраняем контракт базового класса:
Токен отмены передаём дальше без замены, чтобы отмена вызывающего кода дошла до сетевого запроса.
Перед отправкой запускаем секундомер и готовим поле статуса:
Переменная status остаётся null, если транспорт завершился исключением до получения ответа. Это позволяет логировать и успешные ответы, и неудачные попытки соединения.
Выполняем запрос через следующий обработчик и запоминаем статус:
Ответ возвращается вызывающему коду без чтения или изменения его тела. Блок finally выполняется и после ответа, и при сетевом исключении.
В finally записываем метод, статус и длительность:
Регистрируем обработчик как transient и добавляем его в именованный клиент:
Фабрика создаёт цепочки обработчиков для клиентов, поэтому один экземпляр DelegatingHandler нельзя переиспользовать в нескольких цепочках.
➡️ C# Ready | #практика
Когда несколько сервисов обращаются к внешним 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 нельзя переиспользовать в нескольких цепочках.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤4👍4🔥3
Почему Task.Run не делает синхронный ввод-вывод асинхронным?
Оборачивать блокирующее чтение файла в Task.Run кажется способом освободить текущий поток, но ожидание всё равно занимает поток из пула:
Операция остаётся синхронной. Мы лишь переносим блокирующее ожидание на другой поток, поэтому под нагрузкой такие обёртки могут быстрее исчерпать пул.
Для чтения файла используем API с настоящей асинхронной операцией:
Пока система ждёт данные, рабочий поток не занят синхронным чтением. Переданный токен позволяет отменить операцию, если результат больше не нужен.
Токен отмены передаём и в сам метод чтения, и дальше по цепочке:
Для CPU-вычислений Task.Run может быть уместен, например чтобы не блокировать UI-поток:
Синхронная функция должна сама проверять отмену, потому что токен не прерывает уже запущенный код:
Независимые операции с асинхронным API запускаем напрямую и ожидаем вместе:
Так ожидание I/O не расходует потоки на блокировку. В серверном коде Task.Run вокруг обычного ввода-вывода чаще добавляет переключение потоков, а не пропускную способность.
➡️ C# Ready | #совет
Оборачивать блокирующее чтение файла в 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 вокруг обычного ввода-вывода чаще добавляет переключение потоков, а не пропускную способность.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤5👍4🔥2