Вы когда-нибудь пытались использовать метод
Console.ReadLine() в онлайн-компиляторе C#? Да, звучит как извращение, но давайте углубимся в проблему.Метод
Console.ReadLine() позволяет вашему приложению ожидать ввода пользователя в консоли — это основной инструмент для взаимодействия с пользователем в командной строке. Но когда вы пытаетесь использовать этот метод в браузерной среде, появляется проблема: браузеры просто не поддерживают работу с консолью напрямую. Атрибут
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")] прямо в документации говорит: «Не поддерживайте это в браузере!»Браузер — это отличное место для быстрых экспериментов, но когда речь идет о полноценной разработке, лучше использовать локальные инструменты: Visual Studio или Visual Studio Code.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
😢18❤6🥱2😁1
🪚 Швейцарский нож для коллекций коллекций
Коллекция коллекций — частая головная боль. Пришли батчи из очереди, загрузили страницы из API, получили результаты от нескольких потоков.
Одна строка вместо циклов и временных списков:
Когда использовать
• Батчи сообщений из Kafka/RabbitMQ → один список для обработки
• Постраничные данные из API → единая коллекция
• Результаты параллельных задач → склеить всё вместе
SelectMany сохраняет порядок. Если у вас [1,2], [3,4], [5,6] — получите [1,2,3,4,5,6]. Критично для событий, логов, приоритетных очередей.
🐸 Библиотека шарписта
#sharp_view
Коллекция коллекций — частая головная боль. Пришли батчи из очереди, загрузили страницы из API, получили результаты от нескольких потоков.
Одна строка вместо циклов и временных списков:
var all = batches.SelectMany(b => b).ToList();
Когда использовать
• Батчи сообщений из Kafka/RabbitMQ → один список для обработки
• Постраничные данные из API → единая коллекция
• Результаты параллельных задач → склеить всё вместе
SelectMany сохраняет порядок. Если у вас [1,2], [3,4], [5,6] — получите [1,2,3,4,5,6]. Критично для событий, логов, приоритетных очередей.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10❤2🥱2
Когда вы пишете SELECT * FROM users WHERE email = 'user@example.com' без индекса на поле email, база данных выполняет Full Table Scan — последовательно проверяет каждую строку таблицы.
Если в таблице 10 записей — не страшно. Но когда их миллион база читает миллион строк, чтобы найти одну нужную.
Производительность деградирует нелинейно. С ростом данных время выполнения запроса растёт экспоненциально. Запрос, который работал за 10мс на тестовой базе, в продакшене может выполняться минутами.
Как решить проблему
• Добавить индекс
Самое очевидное решение — создать индекс на нужное поле:
CREATE INDEX idx_users_email ON users(email);
• Составные индексы
Если фильтруете по нескольким полям, используйте составной индекс. Порядок полей важен — самое селективное поле должно быть первым:
CREATE INDEX idx_users_status_created ON users(status, created_at);
• Функциональные индексы
Для запросов с функциями создавайте индексы на выражения:
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
• Партиционирование
Для огромных таблиц разбейте данные на партиции по дате или другому критерию — база будет сканировать только нужные партиции, а не всю таблицу.
• Денормализация и кэширование
Иногда проще продублировать данные или закэшировать результаты частых запросов в Redis/Memcached, чем постоянно гонять тяжёлые запросы по базе.
Индексы — не бесплатны. Они занимают место на диске и замедляют INSERT/UPDATE/DELETE операции. Не нужно индексировать всё подряд — создавайте индексы осознанно.
Это уже не «Hello World» писать. Для таких решений нужно знать не только язык. Подтянуть архитектуру можно на нашем интенсиве.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍10❤6🥱1
🛠 Почему dynamic не заслуживает пожизненного бана
Представьте: кто-то в команде написал код с dynamic, получил RuntimeBinderException в продакшене, и теперь это слово под запретом навсегда.
Это самая ленивая политика в истории .NET. Вместо того чтобы разобраться с проблемой, команды просто заклеивают её скотчем из запретов.
В чём подвох
Когда вы запрещаете dynamic, разработчики не перестают решать те же задачи. Они просто переходят на рефлексию. И получается вот:
800 строк рефлексии с silent nulls и магическими строками. Риски те же, код хуже, багов больше.
Когда dynamic действительно нужен
В 95% случаев dynamic — избыточен. Но есть сценарии, где он просто незаменим:
• Плагин-системы
Ваше приложение загружает расширения, которые вы не компилировали вместе с основным кодом. У вас нет доступа к типам на этапе компиляции. dynamic позволяет вызвать
• Скриптинг
Админы пишут небольшие скрипты для настройки логики — расчёт цен, трансформация данных. Вам не нужны 20 статических классов. Вам нужна гибкость с контролем.
• Duck typing в тестах
Когда вы тестируете поведение, а не типы. Не важно, какой это класс — важно, что он умеет делать
Проблема не в dynamic. Проблема в бесконтрольном доступе. Если вы валидируете имена методов, ограничиваете доступ, логируете вызовы и ставите таймауты — вы в безопасности. Возможно, даже в большей, чем с тем лабиринтом из reflection, который живёт в половине вашего кода плагинов.
🐸 Библиотека шарписта
#sharp_view
Представьте: кто-то в команде написал код с dynamic, получил RuntimeBinderException в продакшене, и теперь это слово под запретом навсегда.
Это самая ленивая политика в истории .NET. Вместо того чтобы разобраться с проблемой, команды просто заклеивают её скотчем из запретов.
В чём подвох
Когда вы запрещаете dynamic, разработчики не перестают решать те же задачи. Они просто переходят на рефлексию. И получается вот:
// Вместо одной строки
dynamic plugin = LoadPlugin("RenderEngine");
plugin.Render(data);
// Пишем вот это
var pluginType = plugin.GetType();
var method = pluginType.GetMethod("Render");
if (method == null) throw new InvalidOperationException("Method not found");
method.Invoke(plugin, new object[] { data });
800 строк рефлексии с silent nulls и магическими строками. Риски те же, код хуже, багов больше.
Когда dynamic действительно нужен
В 95% случаев dynamic — избыточен. Но есть сценарии, где он просто незаменим:
• Плагин-системы
Ваше приложение загружает расширения, которые вы не компилировали вместе с основным кодом. У вас нет доступа к типам на этапе компиляции. dynamic позволяет вызвать
plugin.Calculate() без танцев с reflection.• Скриптинг
Админы пишут небольшие скрипты для настройки логики — расчёт цен, трансформация данных. Вам не нужны 20 статических классов. Вам нужна гибкость с контролем.
• Duck typing в тестах
Когда вы тестируете поведение, а не типы. Не важно, какой это класс — важно, что он умеет делать
GetPrice(). Не нужно создавать фейковые интерфейсы ради компилятора.Проблема не в dynamic. Проблема в бесконтрольном доступе. Если вы валидируете имена методов, ограничиваете доступ, логируете вызовы и ставите таймауты — вы в безопасности. Возможно, даже в большей, чем с тем лабиринтом из reflection, который живёт в половине вашего кода плагинов.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤11🥱2
Чтобы не упасть на удалении пробелов по краям строки можно использовать ванлайнер:
var clean = input?.Trim() ?? string.Empty;
•
?. вызывает Trim() только если input не null, иначе возвращает null•
?? подставляет пустую строку, если результат nullИтог: строка очищена от пробелов, либо получаем пустую строку — без риска.
Откройте своим друзьям путь в айти с помощью нашего курса по основам IT для непрограммистов. До конца октября со скидкой!
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🥱23👍7❤3
Представьте: у вас API со списком заказов. Клиент может сортировать по любому полю через параметры запроса. Без динамики пришлось бы писать switch на все варианты:
IQueryable<Order> query = context.Orders;
query = sortBy switch
{
"total_asc" => query.OrderBy(o => o.TotalAmount),
"total_desc" => query.OrderByDescending(o => o.TotalAmount),
"date_asc" => query.OrderBy(o => o.CreatedAt),
"date_desc" => query.OrderByDescending(o => o.CreatedAt),
// ещё 20 полей...
};
С динамической сортировкой код становится лаконичным:
var orders = context.Orders
.Where(o => o.Status == OrderStatus.Completed)
.OrderBy("TotalAmount DESC")
.ThenBy("CreatedAt ASC");
Имя поля и направление сортировки передаются строкой. Всё остается IQueryable — запрос уходит в базу, а не выполняется в памяти.
Можно комбинировать несколько уровней сортировки, передавая массив строк от клиента.
Если строка сортировки приходит от пользователя, нужна валидация. Иначе можно получить exception на несуществующем поле или, хуже, уязвимость.
Попробовать либу:
dotnet add package System.Linq.Dynamic.Core
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔12❤9👍2
Когда вы работаете с делегатами в C#, есть одна особенность, которая может превратить обычный делегат в мощный инструмент. Речь о multicasting — возможности связать с одним делегатом сразу несколько методов.
Представьте, что у вас есть делегат. Обычно он указывает на один метод. Но в C# делегаты можно комбинировать:
public delegate void NotifyHandler(string message);
NotifyHandler handler = LogToConsole;
handler += SendEmail;
handler += SaveToDatabase;
handler("Пользователь авторизовался");
Когда вы вызовете handler, все три метода выполнятся последовательно. Именно эта возможность называется мультикаст.
Основной сценарий — реализация паттерна Observer без лишних сложностей. Вы подписываете несколько обработчиков на одно событие, и все они получают уведомление.
Важные детали
• Методы вызываются в том порядке, в котором вы их добавили. Но полагаться на конкретный порядок — плохая практика. Ваши обработчики должны быть независимыми.
• Если делегат возвращает значение, вы получите результат только от последнего метода в цепочке.
• Если один из методов выбросит исключение, остальные не выполнятся.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5🥱3❤1👍1
Помните те времена, когда каждый метод начинался с целой простыни проверок входных параметров? Копипаста
if (string.IsNullOrEmpty(...)) была ежедневной рутиной:public void ProcessUser(string name, string email, int age)
{
if (string.IsNullOrEmpty(name))
throw new ArgumentException("Value cannot be null or empty.", nameof(name));
if (string.IsNullOrEmpty(email))
throw new ArgumentException("Value cannot be null or empty.", nameof(email));
if (age < 0)
throw new ArgumentOutOfRangeException(nameof(age), "Value must be non-negative.");
// Наконец-то бизнес-логика!
}
Код становился шумным, а реальная логика терялась в океане проверок. Каждый разработчик писал по-своему, сообщения об ошибках отличались, а про опечатки в
nameof() вообще молчим.Теперь всё это превращается в лаконичные однострочники:
public void ProcessUser(string name, string email, int age)
{
ArgumentException.ThrowIfNullOrEmpty(name);
ArgumentException.ThrowIfNullOrEmpty(email);
ArgumentOutOfRangeException.ThrowIfNegative(age);
}
Стандартная библиотека предлагает методы на все случаи жизни:
// Проверки на null
ArgumentNullException.ThrowIfNull(user);
// Числовые диапазоны
ArgumentOutOfRangeException.ThrowIfNegative(temperature);
ArgumentOutOfRangeException.ThrowIfZero(divisor);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count);
// Сравнения
ArgumentOutOfRangeException.ThrowIfGreaterThan(progress, 100);
ArgumentOutOfRangeException.ThrowIfLessThan(quantity, 1);
ArgumentOutOfRangeException.ThrowIfEqual(status, Status.Invalid);
ArgumentOutOfRangeException.ThrowIfNotEqual(version, expectedVersion);
Эти методы — не просто синтаксический сахар. Они воплощают принцип fail-fast: обнаруживай проблемы немедленно, не позволяй невалидным данным распространяться по системе.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍26❤5🥱4❤🔥2🤔2
Метод Enum.TryParse кажется идеальным инструментом для безопасного парсинга строк в enum — он не бросает исключения и возвращает bool, сигнализируя об успехе или неудаче операции.
Но у этого метода есть неочевидное поведение, которое может привести к багам.
Представьте ситуацию: пользователь передаёт статус заказа через API, вы парсите его через
TryParse, получаете true, уверенно обрабатываете заказ... и внезапно обнаруживаете в базе статус со значением 999, которого в вашем енаме вообще не существует.Enum.TryParse возвращает true даже для несуществующих значений enum:public enum OrderType
{
Cool = 0,
NotCool = 1
}
// Парсим значение, которого НЕТ в enum
Enum.TryParse("999", out OrderType type);
// ✓ Вернёт TRUE
// ✓ day = (OrderType)999
// ✗ Но 999 не определён в OrderType!
Console.WriteLine($"Результат: {type}"); // Вывод: 999
TryParse проверяет только возможность конвертации строки в числовой тип, а не валидность значения для конкретного enum.Решение
Добавьте проверку через Enum.IsDefined:
if (Enum.TryParse("999", out OrderType type) &&
Enum.IsDefined(typeof(OrderType), type))
{
// Здесь значение гарантированно валидно
} else {
// 999 будет правильно отклонено
}Enum.IsDefined использует рефлексию и может быть медленным в hot path. Альтернативы:
// Для hot path: кешируем валидные значения
private static readonly HashSet<OrderType> ValidValues =
new(Enum.GetValues<OrderType>());
public static bool IsValid(OrderType value) =>
ValidValues.Contains(value); // Быстрее IsDefined
// Для непрерывных enum: проверка диапазона
public static bool IsValid(OrderType value) =>
(int)value >= 0 && (int)value <= 1; // Самый быстрый
Microsoft спроектировали это так намеренно, поскольку C# позволяет приводить любое число к типу енамки без ограничений. Это даёт гибкость, но требует от разработчика дополнительной бдительности.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
👍20🤩12❤2😁2
📁 Создаём директории правильно
Частая задача: нужно убедиться, что папка существует перед сохранением файла. Многие пишут проверку через Directory.Exists, но есть проще.
Идиоматичный способ:
🐸 Библиотека шарписта
#sharp_view
Частая задача: нужно убедиться, что папка существует перед сохранением файла. Многие пишут проверку через Directory.Exists, но есть проще.
Идиоматичный способ:
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
Directory.CreateDirectory идемпотентна — не бросает исключение, если директория уже есть. Метод просто ничего не делает и возвращает DirectoryInfo. Поэтому проверка через Exists избыточна и добавляет лишний вызов файловой системы.Path.GetDirectoryName может вернуть null для корневых путей или некорректных строк. Поэтому null-forgiving оператор ! используется, когда вы точно знаете, что путь валидный. #sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤10
Когда работаешь с идентификаторами, хешами или логами, часто нужно быстро достать последние символы из строки.
Для получения последних 10 символов используется оператор диапазона:
var last10 = text[^10..];
Такой способ делает код короче и легче для чтения.
Но есть один момент — если строка короче десяти символов, программа выбросит ошибку.
Чтобы этого избежать, добавьте проверку:
var last10 = text.Length >= 10 ? text[^10..] : text;
Теперь код работает безопасно даже с короткими строками.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤14🥱3🌚2👾2
Дедлоки редко проявляются в ходе разработки, но часто всплывают под реальной нагрузкой в продакшне. Дедлок — ситуация, когда две или более сессии базы данных блокируют друг друга, ожидая освобождения ресурсов.
В итоге SQL Server прерывает одну из транзакций:
SqlException: Transaction (Process ID xx) was deadlocked on resources...
Пример простого дедлока: два контекста одновременно меняют одну строку и пытаются сохранить изменения. Один запрос блокирует строку, второй вызывает дедлок.
await using var context1 = new AppDbContext();
await using var context2 = new AppDbContext();
var order1 = await context1.Orders.FindAsync(1);
var order2 = await context2.Orders.FindAsync(1);
order1.Status = "Paid";
order2.Status = "Shipped";
await context1.SaveChangesAsync(); // Блокирует строку
await context2.SaveChangesAsync(); // Возможен deadlock
Стратегии предотвращения
• Короткие транзакции
Минимизируйте работу внутри транзакции:
await using var tx = await context.Database.BeginTransactionAsync();
var order = await context.Orders.FindAsync(orderId);
order.Status = "Paid";
await context.SaveChangesAsync();
await tx.CommitAsync();
Избегайте сетевых вызовов и посторонних запросов перед коммитом.
• Правильные индексы
Отсутствие индексов приводит к сканированию таблиц и длительным блокировкам. Используйте SQL Profiler для мониторинга.
• Оптимальный уровень изоляции
ReadCommitted обычно достаточен для большинства операций:
await using var tx = await context.Database.BeginTransactionAsync(
IsolationLevel.ReadCommitted);
• Логика повторов
Используйте встроенную политику retry в EF Core:
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString, sql =>
sql.EnableRetryOnFailure(
maxRetryCount: 3,
maxRetryDelay: TimeSpan.FromSeconds(5),
errorNumbersToAdd: null)));
• RowVersion для контроля конкурентности
public class Order
{
public int Id { get; set; }
public string Status { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
EF Core выбросит
DbUpdateConcurrencyException при конфликте изменений.Дедлоки не баги, а следствие паттернов конкурентного доступа.
#sharp_view
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7👍5
Даже самая оптимизированная система не застрахована от 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
❤5🔥3👍2