Библиотека шарписта | C#, F#, .NET, ASP.NET
22.5K subscribers
2.46K photos
39 videos
85 files
4.67K links
Все самое полезное для C#-разработчика в одном канале.

По рекламе: @proglib_adv

Учиться у нас: https://proglib.io/w/b60af5a4

Для обратной связи: @proglibrary_feeedback_bot

РКН: https://gosuslugi.ru/snet/67a5c81cdc130259d5b7fead
Download Telegram
⚡️ Deadlock в EF Core: как восстановиться

Даже самая оптимизированная система не застрахована от deadlock на 100%. При высоких нагрузках, пиковых моментах или редких edge-case сценариях они всё равно могут возникнуть. Важно не предотвращение на 100% (это невозможно), а грамотное восстановление.

• Умный ретрай с экспоненциальной задержкой

Простой повтор без задержки только усугубит ситуацию. Используйте экспоненциальную задержку:
public async Task<bool> SaveWithRetryAsync(DbContext context, int maxRetries = 3)
{
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
await context.SaveChangesAsync();
return true;
}
catch (DbUpdateException ex) when (IsDeadlock(ex))
{
if (attempt == maxRetries - 1)
throw;

// Экспоненциальная задержка: 100ms, 200ms, 400ms
var delay = TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt));
await Task.Delay(delay);

Console.WriteLine($"Deadlock detected, retry {attempt + 1}/{maxRetries}");
}
}

return false;
}

bool IsDeadlock(Exception ex) =>
ex.InnerException is SqlException sqlEx &&
(sqlEx.Number == 1205 || // Deadlock victim
ex.InnerException.Message.Contains("deadlocked"));


• Ограничение параллелизма

Если ваше приложение обрабатывает большие пакеты данных, ограничьте количество одновременных операций:
public class OrderProcessor
{
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5); // Макс 5 одновременно

public async Task ProcessOrdersAsync(List<int> orderIds)
{
var tasks = orderIds.Select(id => ProcessWithSemaphoreAsync(id));
await Task.WhenAll(tasks);
}

private async Task ProcessWithSemaphoreAsync(int orderId)
{
await _semaphore.WaitAsync();
try
{
await using var context = new AppDbContext();
var order = await context.Orders.FindAsync(orderId);
order.Status = "Processed";
await SaveWithRetryAsync(context);
}
finally
{
_semaphore.Release();
}
}
}


• Логирование и анализ

Недостаточно просто повторить — нужно понять причину:
catch (DbUpdateException ex) when (IsDeadlock(ex))
{
_logger.LogWarning(ex,
"Deadlock on order {OrderId}, attempt {Attempt}/{MaxAttempts}",
orderId, attempt + 1, maxRetries);

// Отправьте метрику в мониторинг
_telemetry.TrackMetric("Deadlocks", 1, new Dictionary<string, string>
{
["Entity"] = "Order",
["Operation"] = "Update"
});
}


• Захват графов блокировок

SQL Server создаёт подробные графы взаимоблокировок. Настройте их захват:
-- Extended Events для захвата deadlock
CREATE EVENT SESSION [DeadlockMonitoring] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file(SET filename = N'C:\Logs\Deadlocks.xel')
WITH (MAX_MEMORY = 4096 KB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS);

ALTER EVENT SESSION [DeadlockMonitoring] ON SERVER STATE = START;


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

Чтобы строить надёжные решения нужно знать архитектуру и уметь её спроектировать. Для этого можно пройти наш интенсив по архитектуре и шаблонам проектирования. Осталось всего 3 дня скидок!

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
17👍5🔥4
👨‍💻 Ленивый фильтр больших файлов

Когда файл огромный, главное — не тянуть всё в память.

Пример:
var errors = File.ReadLines(logPath).Where(l => l.Contains("ERROR"));


ReadLines читает файл построчно, не загружая всё сразу.

Не добавляйте .ToList(), если хочется сохранить ленивость — иначе всё материализуется в памяти.

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥141
👨‍💻 Worker Services и Channels вместо сторонних библиотек

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

Обычно первая мысль — взять Hangfire, Quartz или Azure Functions. Но .NET 9 предоставляет встроенные примитивы, которые отлично справляются с этой задачей самостоятельно.

Традиционные подходы — блокирующая коллекция и самописные очереди имеют недостатки: требуют ручной синхронизации, рискуют блокировкой потоков, не имеют встроенной поддержки обратного давления.

Реализуем очередь задач с помощью каналов и фоновые сервисы

Интерфейс и реализация очереди:
public interface IBackgroundTaskQueue
{
ValueTask QueueAsync(Func<CancellationToken, Task> workItem);
ValueTask<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken);
}

public class BackgroundTaskQueue : IBackgroundTaskQueue
{
private readonly Channel<Func<CancellationToken, Task>> _queue;

public BackgroundTaskQueue(int capacity = 100)
{
var options = new BoundedChannelOptions(capacity)
{
SingleReader = false,
SingleWriter = false,
FullMode = BoundedChannelFullMode.Wait
};
_queue = Channel.CreateBounded<Func<CancellationToken, Task>>(options);
}

public async ValueTask QueueAsync(Func<CancellationToken, Task> workItem)
=> await _queue.Writer.WriteAsync(workItem);

public async ValueTask<Func<CancellationToken, Task>> DequeueAsync(CancellationToken cancellationToken)
=> await _queue.Reader.ReadAsync(cancellationToken);
}


Фоновые сервисы для обработки:
public class BackgroundWorker : BackgroundService
{
private readonly IBackgroundTaskQueue _taskQueue;
private readonly ILogger<BackgroundWorker> _logger;

public BackgroundWorker(IBackgroundTaskQueue taskQueue, ILogger<BackgroundWorker> logger)
{
_taskQueue = taskQueue;
_logger = logger;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Фоновый worker запущен");

while (!stoppingToken.IsCancellationRequested)
{
var workItem = await _taskQueue.DequeueAsync(stoppingToken);
try
{
await workItem(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Ошибка при выполнении задачи");
}
}

_logger.LogInformation("Worker останавливается...");
}
}


Использование в контроллере:
app.MapPost("/send-email", async (IBackgroundTaskQueue queue) =>
{
await queue.QueueAsync(async token =>
{
await Task.Delay(1000, token); // имитация отправки
Console.WriteLine($"Email отправлен в {DateTime.UtcNow}");
});
return Results.Accepted();
});


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

Когда использовать этот подход

Подходит:

• Внутренняя обработка задач в приложении
• Нет нужды в UI-панели мониторинга
• Self-contained сервисы

Возьмите библиотеку, если:

• Нужна распределённая обработка
• Требуется веб-интерфейс для управления
• Задачи должны пережить перезагрузку приложения

➡️ Источник

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍17
👀 Управление доступом в .NET

Если вы работаете с авторизацией в .NET и используете Identity, но всё ещё не совсем понимаете, как правильно организовать систему разрешений — есть видео, которое стоит посмотреть.

В нём конкретный пример: как взять стандартный ASP.NET Core Identity и использовать его для управления доступом. Ничего экзотического — обычная setup с JWT и кастомным authorization handler.

➡️ Посмотреть видео

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍15
⚡️ Пять команд .NET, без которых неудобно

Бывает, нужно быстро собрать, запустить или опубликовать проект без запуска тяжелой IDE, особенно в пайплайне или на сервере разработчика. Для этого хватает нескольких команд из .NET CLI, которые работают одинаково на всех платформах.

Проверить установку и окружение поможет команда dotnet --info, она покажет версии SDK и рантаймов и архитектуру хоста.

Сборка проекта выполняется командой dotnet build, по умолчанию в конфигурации Debug и с выводом артефактов в bin, причем команда учитывает инкрементальные изменения для скорости.

Запуск приложения через dotnet run объединяет сборку и старт процесса, что эквивалентно кнопке Run в IDE и удобно для ручных проверок локально.

Горячая перезагрузка из терминала через dotnet watch отслеживает изменения файлов и повторно запускает приложение с Hot Reload без ручного рестарта, что ускоряет цикл правка и проверка.

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

Для развертывания используйте dotnet publish, который соберет релиз и положит готовый к деплою набор файлов в папку publish, включая веб приложения и сервисы.

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
10👍1
📎 Когда тип спасает

Представьте, что вы пришли в магазин и продавец говорит: «Вот вам что-то. Не знаю что. Может быть, помидоры, может быть, кирпич, может быть, счастье. Узнаете, когда вернётесь домой». Вот такое же ощущение от IActionResult без типов.

Проблема с IActionResult:
[HttpGet("{id:guid}")]
public async Task<IActionResult> GetById(Guid id, ISender mediator, CancellationToken ct)
{
var dto = await mediator.Send(new GetUserQuery(id), ct);
return dto is null ? NotFound() : Ok(dto);
}


Возвращаемый тип — IActionResult. Просто интерфейс. Ничего конкретного. Кто читает этот код, не знает, что именно вернётся. Может быть UserDto, может быть ошибка, может быть что угодно.

Вот так выглядит сгенерированная Swagger документация:
{
"responses": {
"200": { "description": "Success" }
}
}


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

Решение: ActionResult<T>
[HttpGet("{id:guid}")]
public async Task<ActionResult<UserDto>> GetById(Guid id, ISender mediator, CancellationToken ct)
{
var dto = await mediator.Send(new GetUserQuery(id), ct);
return dto is null ? NotFound() : Ok(dto);
}


Возвращаемый тип — ActionResult<UserDto>. Конкретно и ясно. Читающий код понимает мгновенно: метод возвращает UserDto при успехе или ошибку. Swagger генератор видит типы и строит правильную документацию.

IActionResult можно использовать, когда метод не возвращает тело, к примеру ответ 204.

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
13👍6
👨‍💻 Demystifier: когда стек-трейс не радует

Откройте логи ошибки из продакшена. Видите стек-трейс? Он полон <>c__DisplayClass2_0, d__5``1.MoveNext() и прочих артефактов, которые сгенерировал компилятор. Это не ошибка — это просто то, как .NET преобразует современный C# в IL. Но это делает поиск проблемы медленнее, чем нужно.

Demystifier восстанавливает оригинальный вид кода:

• Вместо ValueTuple``2 param показывает (string val, bool) param
• Вместо Func``1 показывает Func<string>
async методы помечает как async Task<string>
• Локальные функции и лямбды показывает с контекстом

Как применить

Вызовите exception.Demystify() и передайте результат:
try { /* ваш код */ }
catch (Exception ex)
{
logger.LogError(ex.Demystify(), "Ошибка");
}


Анализ стека стоит ресурсов. На высоконагруженной системе вызывать Demystify для каждого исключения неэкономно. Используйте его выборочно — для критических путей, для отладки в development окружении или для редких, но важных ошибок.

➡️ Репозиторий либы

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍121
📎 Как считать повторения без условных операторов

Нужно посчитать, сколько раз каждое значение встречается в данных. Логично напрашивается проверка:
if (counts.ContainsKey(key))
counts[key]++;
else
counts[key] = 1;


Но есть способ проще:
counts[key] = counts.GetValueOrDefault(key) + 1;


Метод GetValueOrDefault возвращает значение, если ключ есть, или значение по умолчанию (для int это 0). Затем добавляем единицу. Никаких условных операторов, одна строка.

Если можете — используйте LINQ:
var counts = items.GroupBy(x => x.Key)
.ToDictionary(g => g.Key, g => g.Count());


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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10
🤩 Lorem ipsum, который имитирует научный вздор

Представьте: нужно заполнить макет контентом, чтобы дизайнер видел, как всё выглядит. Берёте Lorem ipsum. Но он выглядит как набор слов. А если нужен текст, который с первого взгляда кажется серьезным, но при внимательном чтении — полная чушь? Вот для этого WaffleGenerator.

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

Как применить

Просто:
var text = WaffleEngine.Html(paragraphs: 2, includeHeading: true);
var text = WaffleEngine.Text(paragraphs: 1, includeHeading: true);
var markdown = WaffleEngine.Markdown(paragraphs: 1, includeHeading: true);


Поддерживает три формата: HTML, обычный текст и Markdown.

➡️ Попробовать либу

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥145
👨‍💻 IOptions vs IOptionsSnapshot vs IOptionsMonitor

В .NET существует несколько интерфейсов для работы с настройками: IOptions, IOptionsSnapshot и IOptionsMonitor. Помогаем разобраться, какой из них когда нужен.

IOptions — загружает конфигурацию однажды при запуске и всегда возвращает одно и то же значение:
public class MyService
{
private readonly MySettings _settings;

public MyService(IOptions<MySettings> options)
{
_settings = options.Value;
}

public void PrintSettings()
{
Console.WriteLine($"API Key: {_settings.ApiKey}");
}
}


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

IOptionsSnapshot — создаётся на каждый запрос или скоуп и предоставляет новые значения при каждом новом запросе:
public class MyScopedService
{
private readonly MySettings _settings;

public MyScopedService(IOptionsSnapshot<MySettings> options)
{
_settings = options.Value;
}

public void PrintSettings()
{
Console.WriteLine($"Timeout: {_settings.Timeout}");
}
}


Подходит для веб-приложений, где конфигурация может меняться между запросами.

IOptionsMonitor — позволяет подписаться на изменения и получать обновления конфигурации в реальном времени:
public class MyMonitorService
{
private MySettings _settings;

public MyMonitorService(IOptionsMonitor<MySettings> monitor)
{
_settings = monitor.CurrentValue;
monitor.OnChange(updatedSettings =>
{
_settings = updatedSettings;
Console.WriteLine("Settings updated!");
});
}

public void PrintSettings()
{
Console.WriteLine($"LogLevel: {_settings.LogLevel}");
}
}


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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍183
⚡️ Когда важна производительность

В C# цикл foreach часто воспринимается как самая удобная и красивая форма для обхода коллекций. Но в вопросах производительности foreach не всегда оптимален.

Почему лучше избегать foreach:

foreach использует итератор — для массивов его оптимизируют, но для списков, словарей и других коллекций создаётся объект или struct-итератор.

• под капотом foreach может создавать временные объекты, что увеличивает нагрузку на сборщик мусора и снижает производительность.

foreach всегда выполняет проверки и гарантирует корректность обхода, но это стоит некоторой производительности.

Что использовать вместо foreach:

Цикл for с индексом, особенно для массивов:
for (int i = 0; i < data.Length; i++)
{
// работа с data[i]
}


Span<T> и его методы — для обхода данных без выделения памяти и избыточных проверок:
var span = data.AsSpan();
for (int i = 0; i < span.Length; i++)
{
// работа с span[i]
}


Когда можно использовать foreach:

• Если цикл выполняется нечасто или количество элементов небольшое.

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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔154🥱1
⭐️ Иммутабельность без танцев с бубном

Records — это ссылочный тип, который упростил жизнь разработчикам, привыкшим писать иммутабельные классы вручную. Вот зачем они нужны.

Раньше иммутабельный класс требовал много кода: readonly свойства, отключённые сеттеры, кастомные конструкторы. И всё равно где-то находилась лазейка. С Records это просто исчезает:
public record User(string Name, string Email);

var user = new User("Алексей", "alex@example.com");
// user.Name = "Новое имя"; — Ошибка компиляции


Все свойства автоматически получают init-accessor. Установить значение можно только при создании объекта. Это не просто синтаксический сахар — это гарантия от багов состояния, которые трудно найти.

Если вам нужен объект инициализатор, Records это поддерживают:
public record Product
{
public string Name { get; init; }
public decimal Price { get; init; }
}

var book = new Product { Name = "C# Deep Dive", Price = 49.99M };


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

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍18🥱65
💻 Сравнение по значению автоматом

Вы знаете, что с классами сравнение — это боль? Два объекта с одинаковыми данными считаются разными, потому что сравнивается идентичность. Иначе работают Records:
public record Person(string FirstName, string LastName);

var p1 = new Person("Иван", "Иванов");
var p2 = new Person("Иван", "Иванов");

Console.WriteLine(p1 == p2); // true


Без переопределения Equals, без GetHashCode, без перегрузки оператора ==. Record сам сравнит все свойства и даст правильный результат.

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

var users = new HashSet<User>();
users.Add(new User("Мария", "maria@example.com"));
users.Contains(new User("Мария", "maria@example.com")); // true — работает!


Value-based семантика — это основа, на которой строятся остальные возможности Records.

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍181
📎 Копирование с with

Records поддерживают выражение with, которое создаёт изменённую копию объекта без изменения оригинала:
public record Person(string FirstName, string LastName);

var original = new Person("Анна", "Смирнова");
var updated = original with { LastName = "Петрова" };

Console.WriteLine(original.LastName); // Смирнова
Console.WriteLine(updated.LastName); // Петрова


Особенно полезно в event sourcing, CQRS или Redux-подобных архитектурах, где вы формируете новые состояния без мутации исходных данных:
public record Order(int Id, string Status);

var first = new Order(1, "Ожидает");
var second = first with { Status = "Обработан" };
var third = second with { Status = "Отправлен" };

// Все три состояния существуют независимо


Это гарантирует, что никаких побочных эффектов, никаких неожиданных изменений в другой части кода. История состояний становится явной.

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
13👍11🥱4
👍 Распаковка данных в одну строку

Records автоматически поддерживают деструкцию благодаря основному конструктору. Это позволяет распаковать свойства объекта прямо при присваивании.

Вместо того чтобы обращаться к каждому свойству отдельно:
public record Point(int X, int Y);

var point = new Point(15, 20);
int x = point.X;
int y = point.Y;


Просто распакуйте:
var (x, y) = new Point(15, 20);
Console.WriteLine($"Координаты: {x}, {y}");


Это особенно удобно в сочетании с pattern matching и switch выражениями:
public record User(string Name, int Age);

if (new User("Боб", 25) is { Age: > 18 })
Console.WriteLine("Взрослый пользователь");

var (name, age) = new User("Алиса", 30);


Вместо трёх строк получаете одну строку и респект от коллег не факт.

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

#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
😢51👍1