Топ-20 помилок .NET-розробників, які я зустрічаю щотижня (і які вам варто виправити вже сьогодні)
Всім привіт! Мене звати Едуард, я .NET Engineer в компанії Infinity Technologies. Протягом останніх років я бачив, як одні й ті самі помилки повторюються знову і знову як у власному коді, так і в роботах колег. Часто ці помилки здаються дрібницями, але саме вони стають причиною непередбачуваних багів, поганої продуктивності і «технічного боргу», який потім боляче повертати.
Тож ловіть мій топ-20 помилок, які потрібно прибрати зі свого коду, щоб стати сильнішим .NET-інженером.
1. Використання DateTime.Now замість абстракції годинника (IClock)
Класика жанру. Здається, що DateTime.Now це безпечний спосіб отримати поточний час. Але проблема в тому, що цей час залежить від системної часової зони.
Уявіть, що у вас мікросервісна архітектура, де частина сервісів живе в UTC, а інші в локальному часі (бо хтось колись так налаштував сервер). Результат баги з часовими зміщеннями, які неможливо стабільно відловити в тестах.
Кейс із практики: Логіка нарахування підписок, яка працювала чудово у нас в Києві, почала «здвигати» дати для клієнтів з Нью-Йорка. Причина DateTime.Now підхоплював локальний час сервера. Рішення винести час у IClock і контролювати джерело в тестах та продакшні.
2. Працюєте на .NET Framework замість LTS версії (.NET 8)?
Так, міграція може бути болючою, особливо якщо проекту 10+ років. Але чим довше ви тягнете з переходом, тим більше втрачаєте:
- Сучасні оптимізації продуктивності (особливо з .NET
6-8). - Безпекові патчі (Framework більше не оновлюється).
- Нові можливості екосистеми (тулінг, бібліотеки).
Кейс із практики: Один з клієнтів Infinity Technologies роками тримав CRM-систему на .NET Framework 4.6. Після переходу на .NET 7 продуктивність API виросла на 30% «з коробки» просто через нові оптимізації рантайму.
3. Логування простим текстом замість Serilog/Seq
У невеликих проєктах логування через Console.WriteLine або прості текстові файли здається нормальним рішенням. Але коли у вас продакшн, де летить 10 000+ запитів на хвилину знайти в логах проблему без структурованого підходу стає майже неможливо.
Кейс із практики: У нас був сервіс, де команда логувала все у текстовий файл. Під час розслідування проблеми з продуктивністю ми втратили пів дня, просто шукаючи потрібні записи. Після переходу на Serilog з вивантаженням у Seq аналітика логів стала займати хвилини.
4. Важка робота в request-потоці ASP.NET
Якщо у вас на request thread’і йде обробка важкої аналітики, генерація PDF чи масивний SQL-запит ви автоматично блокуєте цей потік на весь час виконання задачі. Результат високий latency, зниження RPS, і користувачі бачать спінери замість відповідей.
Кейс із практики: Ми працювали над системою генерації звітів, яка синхронно будувала PDF одразу після кліку користувача. Коли навантаження зросло, сайт почав зависати. Перенесення генерації в background queue (через Hangfire) знизило навантаження в 5 разів.
5. Забуваєте Dispose() для DbContext, HttpClient, Stream?
Це одна з найпідступніших помилок, бо наслідки не завжди проявляються одразу. Забутий Stream може залишити відкритий файл, не звільнений DbContext тримати підключення до бази, а новий HttpClient на кожен запит швидко вичерпати доступні порти.
Кейс із практики: В одному з проєктів команда створювала новий HttpClient для кожного запиту до стороннього API. На тестах все працювало чудово, але на проді почали отримувати помилки «Address already in use». Вирішення використовувати Singleton HttpClient або IHttpClientFactory.
6. Блокуєте async через .Result або .Wait()
У .NET async/await працює так, що якщо ви блокуєте асинхронний виклик через .Result або .Wait(), це може призвести до дедлоків або жорсткого блокування потоків. Особливо небезпечно в ASP.NET, де кожен потік на вагу золота.
Кейс із практики: В одному проєкті був сервіс, який викликав async-метод отримання токена авторизації через .Result. Все працювало, поки навантаження було мінімальним. Але коли зросла кількість запитів, частина потоків почала «залипати» в очікуванні. Після рефакторингу на await проблема зникла, а продуктивність виросла на 20%.
7. Бізнес-логіка всередині LINQ-запитів до EF
Коли ви починаєте писати умовні конструкції, цикли або розгалуження прямо всередині LINQ-запитів до Entity Framework це погано з двох причин:
- Ви блокуєте тестування цієї логіки.
- Ви стаєте заручником того, як EF транслює ваш код у SQL.
Кейс із практики: Один з розробників у нас реалізував логіку ціноутворення (з урахуванням акцій, типів клієнтів і т.д.) прямо у Where-запиті до бази. В результаті, запит до бази став монстром, який важко оптимізувати і неможливо покрити юніт-тестами. Вирішення логіку винесли окремим сервісом, а запити до бази зробили максимально простими.
8. Жорстке прописування конфігів замість IOptions<T> і секретів
Hardcoded налаштування (URLs, connection strings, API-ключі) прямо в коді це не лише погана практика з точки зору безпеки, але й велика проблема для масштабованості проєкту.
Кейс із практики: Під час міграції сервісу на staging середовище команда виявила, що всі URL-адреси були захардкожені прямо в сервісах. Зміна оточення перетворилася на пекло. Перехід на IOptions<T> і винос секретів у Azure Key Vault вирішив проблему раз і назавжди.
9. Service Locator замість нормальної DI-ін’єкції
Service Locator дає ілюзію зручності, бо «можна дістати будь-який сервіс звідки завгодно». Але насправді це приховує залежності класів і робить тести майже неможливими без хака.
Кейс із практики: В одному з проєктів розробники масово тягнули сервіси через Service Locator (ServiceProvider.GetService<T>), і коли прийшов час писати юніт-тести половина класів вимагала моків на 10+ залежностей, які навіть не проглядалися у конструкторі. Після рефакторингу на constructor injection тести стали значно простішими.
10. Вимкнення аналізаторів і автоперевірки стилю коду
«Воно ж мені заважає кодити швидко, ці попередження.» Результат через пару місяців проєкт перетворюється на стилістичний хаос, а баги, які можна було б впіймати на етапі розробки, потрапляють у прод.
Кейс із практики: У невеликій команді вимкнули Roslyn-аналітик «бо заважали працювати». Через два місяці отримали production-багу з null reference, яка могла б бути попереджена nullable-аналізатором. Виправили? Так. Але втратили день продакшн-тайму.
11. God-класи і контролери по 500+ рядків
Коли один клас (особливо контролер) розростається до сотень рядків, це означає лише одне: ви зібрали в ньому все, що могли. Такі «Бог-класи» важко тестувати, важко читати і неможливо ефективно рев’ювити.
Кейс із практики: В одному з проєктів контролер замовлень виріс до 1 200 рядків, обробляючи весь життєвий цикл замовлення, валідацію, платежі і навіть бізнес-правила. Під час чергового рев’ю ми витратили 2 години на обговорення одного контролера. Після розбиття на окремі сервіси (OrderService, PaymentService, ValidationService) рев’ю таких змін почало займати
12. Відсутність ConfigureAwait(false) у бібліотеках
Якщо ви пишете бібліотеки або SDK для інших проектів, не використання ConfigureAwait(false) в асинхронних викликах може призводити до дедлоків, особливо у WPF/WinForms додатках.
Кейс із практики: Ми розробляли бібліотеку для інтеграції з платіжним шлюзом. Усі async-методи не мали ConfigureAwait(false). В результаті, коли клієнт інтегрував бібліотеку в WinForms-додаток, частина інтерфейсу почала «вмирати» через дедлоки. Виправили це, додавши ConfigureAwait(false) у всіх внутрішніх викликах бібліотеки.
13. Ловля System.Exception без повторного викидання
Коли ви ловите System.Exception і нічого з ним не робите (а тим більше не прокидуєте далі), ви маскуєте реальну проблему. Це як бачити, що димить, але заклеїти лампочку індикатора.
Кейс із практики: Один сервіс час від часу падав, але у логах нічого не було. Виявилось, що у catch(Exception ex) був пустий catch-блок без логування і rethrow. Після додавання логування ми знайшли баг у сторонній бібліотеці, який роками «ховався» у цій чорній дірі.
14. Закоментований код у репозиторії
Закоментований код це шум. Він відволікає розробників, ускладнює merge-конфлікти і створює ілюзію «потрібності». Git і так зберігає історію змін.
Кейс із практики: В одному проекті в репозиторії залишались закоментовані фрагменти старої логіки, яка вже не використовувалась, але «раптом ще стане в нагоді». Під час міграції на нову архітектуру merge-конфлікти з цим «мертвим» кодом затягнули процес об’єднання гілок на кілька днів.
15. Ігнорування CancellationToken у асинхронних завданнях
Без CancellationToken користувачі не зможуть скасувати довгі або непотрібні операції. Ресурси витрачаються даремно, ви ризикуєте накопиченням «завислих» тасків і виснаженням системи.
Кейс із практики: У проекті з імпортом великих Excel-файлів не було підтримки CancellationToken. Коли користувачі завантажували файли по 500 000 рядків і розуміли, що обрали не той файл, вони не мали можливості зупинити обробку. В результаті сервер просто зжер усі потоки. Після додавання підтримки відміни завдання через CancellationToken, система почала вести себе набагато стабільніше під навантаженням.
16. Відсутність #nullable enable
Nullable Reference Types у C# це не просто нова фіча, це діагностичний інструмент, який попереджає вас про потенційні NullReferenceException ще на етапі компіляції. Вимкнувши цю опцію, ви добровільно відмовляєтесь від захисту.
Кейс із практики: В одному з проєктів працювали з API, яке іноді повертало null у деяких полях JSON. Без включеного Nullable Reference Types ми зловили NRE у продакшені. Після включення #nullable enable компілятор одразу показав, де саме ми «довіряли» даним без перевірок.
17. Ініціалізація HttpClient через new замість Singleton/IHttpClientFactory
Кожного разу, коли ви створюєте новий екземпляр HttpClient через new, ви відкриваєте нове TCP-з’єднання. Якщо це відбувається часто, ваші порти швидко вичерпуються, а продуктивність падає.
Кейс із практики: У нас був сервіс, який викликав стороннє API з new HttpClient() для кожного запиту. При навантаженні понад 1000 RPS сервер почав «падати» через портове виснаження. Після рефакторингу на IHttpClientFactory проблема зникла повністю, а середній час відповіді знизився вдвічі.
18. Всі сервіси в одному проєкті без модульності
Коли у вас всі сервіси, ентіті і логіка живе в одному великому проекті без поділу на модулі/шари ви отримуєте архітектурний моноліт, який важко підтримувати. Не плутайте це з монолітною системою це саме про хаос у проєкті.
Кейс із практики: В одному проекті не було окремих шарів для Business Logic, Data Access і Presentation. Будь-яка зміна вимагала реверс-інженерії всього рішення. Після впровадження модульної архітектури (чіткий поділ на Core, Infrastructure, API) швидкість розробки зросла на 30%.
19. Надмірне використання dynamic «бо так простіше»
Dynamic спокушає швидко вирішувати проблему з типами, але як тільки dynamic заходить у ваш код ви втрачаєте типобезпеку, інтелісенс і компілятор більше не ваш друг. Це short-term рішення, яке породжує long-term хаос.
Кейс із практики: Розробник використав dynamic для роботи з JSON, аби «не писати зайві класи». Все працювало, поки структура JSON не змінилась. Замість компіляторної помилки ми отримали runtime-баг на продакшні. Виправили, перейшовши на Typed Models з валідацією.
20. «Мені не потрібні Unit-тести, я і так все перевірив руками»
Ручне тестування це добре для UI і інтеграційних кейсів, але якщо у вас немає юніт-тестів на бізнес-логіку, ви ризикуєте впіймати багу у найнесподіваніший момент. Тест-кейси мають жити поряд з кодом.
Кейс із практики: В одному проекті розробник вважав, що «юніт-тести це для слабаків», поки не зробив рефакторинг обробки знижок, який поламав ціну для преміум-клієнтів. Помітили це лише через тиждень. Якби були юніт-тести баг зловили б одразу.
Замість висновку
Кожна з цих помилок це реальний кейс, який я зустрічав у роботі. Більшість із них не потребують тижнів на виправлення достатньо просто змінити звичку або глянути на код під іншим кутом.
А, може, у вас є власний пункт № 21?
16 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів