Перейти до вмісту

Платформа .NET

З чого складається платформа .NET і як код на C# стає працюючою програмою: версії та їхня підтримка, CLR і JIT, збірки й пакети, способи компіляції. Це фундамент для всіх інших тем розділу.

Junior+

Чим .NET Framework відрізняється від .NET Core і сучасного .NET?

Section titled “Чим .NET Framework відрізняється від .NET Core і сучасного .NET?”
Відповідь

Що це

  • .NET Framework — початкова платформа .NET (з 2002 року) лише для Windows. Остання версія, 4.8.1, вийшла у 2022 році.
  • .NET Core — кросплатформна платформа з відкритим кодом (з 2016 року). З версії 5 (2020) вона називається просто .NET.

Чим відрізняються

.NET Framework .NET 5+
ОС лише Windows Windows, Linux, macOS
Версії на машині одна 4.x, частина ОС кілька поруч
Розвиток лише виправлення нова версія щолистопада

Типова помилка

Вважати .NET 8 новою версією .NET Framework. Це різні продукти: номер 4 пропустили, щоб їх не плутали, а слово Core прибрали, бо тепер це основна гілка.

Як влаштовано

  • Встановлення. Framework — спільний компонент Windows: оновлення ОС змінює поведінку одразу всіх застосунків. Застосунок на .NET прив’язаний до своєї major-версії (net8.0, net10.0), а self-contained-публікація несе середовище виконання із собою.
  • Проєкти. SDK-style .csproj, dotnet CLI і PackageReference. У Framework історично багатослівні проєкти й packages.config.
  • Конфігурація й веб. Замість web.config — appsettings.json, замість HttpModule — middleware ASP.NET Core.

Чого немає в .NET

Технологія Framework Заміна в .NET
Додаткові AppDomain Процеси й контейнери; для плагінів — AssemblyLoadContext
.NET Remoting gRPC, HTTP API
Code Access Security Межі ОС: контейнери, облікові записи
ASP.NET Web Forms ASP.NET Core: Razor Pages, Blazor
Windows Workflow Foundation Вбудованої немає, є сторонній CoreWF

Пастки

  • WinForms і WPF у .NET є, але працюють лише на Windows (цільова платформа на кшталт net10.0-windows).
  • Видалені технології часто виринають не у своєму коді, а глибоко в залежностях.

Правило

Новий код — лише на сучасному .NET. Framework залишають для легасі, яке тримається на Web Forms, WCF-сервері чи WF і яке дорожче переписати, ніж підтримувати.

Під капотом

  • Framework оновлюється на місці: 4.8 замінює 4.7.2 для всіх застосунків на машині. Будь-яка зміна поведінки ризикує зламати чужий код, тому розвиток уперся в сумісність.
  • Сучасний .NET встановлюється side-by-side: кожен застосунок обирає свою версію, а major-реліз може змінювати поведінку. Саме це дало змогу щороку переписувати JIT, GC і бібліотеки.

Ціна

  • З .NET 5 глобалізація на Windows працює через ICU, а не NLS: порівняння й сортування рядків із культурою можуть дати інший результат.
  • System.Drawing.Common з .NET 6 підтримується лише на Windows, а шляхи в Linux чутливі до регістру.
  • Екосистема йде від Framework: EF Core з версії 5.0 на ньому не працює (3.1 — остання версія, яка його підтримує).

Компроміси

Міграція «великим вибухом» ризикована, тому йдуть кроками:

  1. Перевести проєкти на SDK-style і PackageReference.
  2. Спільні бібліотеки зібрати під netstandard2.0 або одразу під обидві платформи.
  3. Знайти несумісні API: platform compatibility analyzer, .NET Upgrade Assistant.
  4. Переносити хости. Веб — поступово: новий застосунок ASP.NET Core з YARP стає перед старим і забирає маршрути по одному.

Як перевірити в продакшені

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

Підсумок: Framework застиг через обов’язкову сумісність, і шлях уперед — поетапна міграція з перевіркою поведінки, а не переписування з нуля.

Приклад
using System.Runtime.InteropServices;
// Яке середовище виконання і яка ОС виконують код
Console.WriteLine(RuntimeInformation.FrameworkDescription);
// .NET Framework: ".NET Framework 4.8.xxxx"
// .NET 10: ".NET 10.0.x"
Console.WriteLine(RuntimeInformation.OSDescription);
Console.WriteLine(RuntimeInformation.ProcessArchitecture); // X64, Arm64
#if NETFRAMEWORK
Console.WriteLine("Скомпільовано під .NET Framework");
#elif NET5_0_OR_GREATER
Console.WriteLine("Скомпільовано під .NET 5+");
#endif
Junior+

Що таке LTS і STS і як обрати версію .NET для проєкту?

Section titled “Що таке LTS і STS і як обрати версію .NET для проєкту?”
Відповідь

Що це

  • LTS (Long Term Support) — парні версії, підтримка три роки.
  • STS (Standard Term Support) — непарні версії, підтримка два роки; до .NET 9 було 18 місяців.
  • Підтримка — це вихід патчів, зокрема виправлень вразливостей. Після її завершення патчів немає.

Версії на жовтень 2026

Версія Тип Підтримка до
.NET 8 LTS 10 листопада 2026
.NET 9 STS 10 листопада 2026
.NET 10 LTS 14 листопада 2028
.NET 11 STS виходить у листопаді 2026

Як користуватися

Версію задає <TargetFramework>net10.0</TargetFramework> у .csproj. Патчі (10.0.1, 10.0.2 і далі) встановлюються без зміни проєкту.

Типова помилка

Залишатися на версії без підтримки, «бо працює».

Як влаштовано

  1. Щолистопада виходить major-версія; треки LTS і STS чергуються.
  2. Майже щомісяця виходить патч. Виправлення безпеки — у другий вівторок місяця (Patch Tuesday).
  3. Застосунок без вбудованого середовища виконання запускається на останньому встановленому патчі своєї версії. На нову major-версію він не переходить, доки не задано RollForward (Major або LatestMajor).
  4. SDK версіонується окремо, за feature bands (10.0.1xx, 10.0.2xx). Свіжий SDK збирає проєкти й під старі версії .NET.

Коли що обирати

Проєкт Версія
Сервіс із частими релізами Остання, LTS або STS
Продукт, який оновлюється рідко LTS
Інструменти збирання Останній SDK, навіть зі STS

Пастки

  • Out-of-band-пакети зі STS-релізу потрапляли в LTS-застосунки через залежності й скорочували їхню підтримку. Тому у 2025 році STS подовжили до 24 місяців.
  • Без global.json розробники й CI збирають проєкт різними SDK.

Правило

Оновлюватися раз на рік на найближчу підтримувану версію, не чекаючи кінця підтримки.

Під капотом

  • Хост (dotnet або запускач застосунку) читає runtimeconfig.json і обирає середовище виконання за політикою roll-forward. За замовчуванням це Minor: останній патч своєї версії.
  • Self-contained-публікація фіксує середовище виконання на момент збирання: патчі приходять лише з перезбиранням.

Ціна

  • Кожна major-версія — це список breaking changes. Стрибок через три версії збирає їх усі одразу, тому щорічний апгрейд дешевший.
  • 10 листопада 2026 одночасно завершується підтримка .NET 8 і .NET 9. Мігрувати одразу на .NET 11 — означає брати реліз, що вийшов того самого місяця.

Компроміси

  • ASP.NET Core, EF Core і Microsoft.Extensions.* версіонуються разом із .NET, і їхні нові major-версії часто вимагають нового .NET. Середовище виконання й залежності оновлюють разом.
  • Образ контейнера з тегом major.minor (mcr.microsoft.com/dotnet/aspnet:10.0) підтягує патчі під час перезбирання. Точний тег дає відтворюваність, але патчі доведеться піднімати вручну або через Dependabot і Renovate.

Як перевірити в продакшені

  • dotnet --list-runtimes і dotnet sdk check показують встановлені версії й те, що вийшло з підтримки.
  • RuntimeInformation.FrameworkDescription у лозі старту показує, на якому патчі насправді працює сервіс.

Підсумок: версію .NET обирають за графіком підтримки й оновлюють за планом, а не коли «зламається».

Приклад
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- .NET 10 — LTS, підтримка до листопада 2028 -->
<TargetFramework>net10.0</TargetFramework>
<!-- Дозволити запуск на новішій major-версії, якщо потрібної немає -->
<RollForward>Major</RollForward>
</PropertyGroup>
</Project>
Окно терминала
dotnet --list-sdks # встановлені SDK
dotnet --list-runtimes # встановлені середовища виконання
dotnet sdk check # що застаріло і що вийшло з підтримки
Middle+

Як виконується код на C#: IL, CLR, JIT?

Section titled “Як виконується код на C#: IL, CLR, JIT?”
Відповідь

Що це

  • IL — проміжна мова. Компілятор C# записує її в збірку .dll разом із метаданими — описом типів і методів.
  • CLR — середовище виконання: завантажує збірки, керує пам’яттю, винятками й потоками.
  • JIT — частина CLR, яка перекладає IL методу в машинний код.

Як це працює

  1. dotnet build — Roslyn компілює .cs-файли в IL.
  2. dotnet run — запускається CLR і завантажує збірку.
  3. Перед першим викликом методу JIT компілює його в код для поточного процесора.
  4. Наступні виклики йдуть одразу в готовий машинний код.

Типова помилка

Думати, що C# інтерпретується, як Python. Процесор завжди виконує машинний код, просто цей код з’являється під час роботи програми.

Як влаштовано

  1. Запускач (dotnet app.dll або app.exe) знаходить hostfxr. Той читає app.runtimeconfig.json і обирає версію середовища виконання.
  2. hostpolicy читає app.deps.json, складає список збірок застосунку й запускає CoreCLR — CLR сучасного .NET.
  3. CLR завантажує збірку й знаходить точку входу.
  4. Кожен метод спочатку викликається через заглушку. Вона запускає JIT і підміняє себе адресою готового коду.
  5. Метод, який жодного разу не викликали, не компілюється ніколи.

Що ще дає CLR

  • Збирання сміття, винятки, завантаження типів, потоки, виклики нативного коду (P/Invoke).
  • CTS — спільну систему типів: C#, F# і VB.NET компілюються в один IL і використовують типи одне одного.

Пастки

  • Generic-код JIT компілює окремо для кожного значущого типу-аргументу й один раз для всіх посилальних. Багато інстанціацій зі структурами — більше машинного коду.
  • Перші запити до сервісу повільніші через JIT і завантаження типів.

Правило

Не плутати повільний старт із повільним кодом: продуктивність вимірюють після прогріву.

Під капотом

  • IL не залежить від процесора, тому JIT генерує код під реальну машину: інструкції AVX2, AVX-512 чи Arm64.
  • В оптимізованому коді JIT знає значення static readonly-полів уже ініціалізованих класів: підставляє їх як константи й викидає мертві гілки.
  • Спочатку JIT робить швидкий неоптимізований код, а гарячі методи потім перекомпільовує з оптимізаціями у фоні (tiered compilation).

Ціна

  • Час старту, CPU на компіляцію і пам’ять під код.
  • У контейнері з лімітом в одне ядро перші секунди після старту сервіс помітно повільніший, і це б’є по автомасштабуванню.

Компроміси

Варіант Старт Обмеження
JIT повільніший немає
ReadyToRun: машинний код плюс IL швидший збірки більші
Native AOT: без JIT найшвидший немає Reflection.Emit і динамічного завантаження

Як перевірити в продакшені

  • dotnet-counters monitor -p <pid> --counters System.Runtime — кількість скомпільованих JIT методів і час у JIT.
  • dotnet-trace з JIT-подіями показує, які методи компілюються на старті.
  • DOTNET_JitDisasm="*Add*" друкує асемблер (з .NET 7 — у звичайних збірках середовища виконання); у BenchmarkDotNet для цього є [DisassemblyDiagnoser].

Підсумок: JIT обмінює час старту на машинний код, оптимізований під реальну машину.

Приклад
Console.WriteLine(Add(2, 3)); // 5
static int Add(int a, int b) => a + b;
// IL методу Add у збірці (видно в ILSpy або на sharplab.io):
// ldarg.0 // покласти a на стек обчислень
// ldarg.1 // покласти b
// add // зняти два значення, покласти суму
// ret // повернути вершину стека
//
// Оптимізований машинний код від JIT (Windows x64):
// lea eax, [rcx+rdx]
// ret
//
// Подивитися самому:
// DOTNET_JitDisasm="*Add*" dotnet run -c Release
Middle+

Що таке .NET Standard і навіщо multi-targeting (TargetFrameworks)?

Section titled “Що таке .NET Standard і навіщо multi-targeting (TargetFrameworks)?”
Відповідь

Що це

.NET Standard — пронумерований набір API (від 1.0 до 2.1), який зобов’язані реалізувати платформи .NET. Це контракт для бібліотек, а не окреме середовище виконання.

Навіщо

Колись існували .NET Framework, .NET Core, Xamarin, Mono, UWP — з різними API. Бібліотеці під .NET Standard не треба знати, на якій із них вона запуститься.

Як користуватися

Хто використовуватиме бібліотеку Ціль
Зокрема .NET Framework netstandard2.0
Лише сучасний .NET net8.0 або net10.0

Типова помилка

Брати netstandard для нового коду, якому .NET Framework не потрібен: втрачаються нові API без жодної вигоди.

Як влаштовано

  • Хто реалізує. netstandard2.0 — .NET Framework 4.6.1+ (Microsoft радить 4.7.2+: у 4.6.1 є проблеми), .NET Core 2.0+, .NET 5+, Mono, Unity. netstandard2.1 — .NET Core 3.0+ і .NET 5+, без .NET Framework.
  • TFM у .NET 5+. net10.0 — кросплатформний API, net10.0-windows — плюс специфіка Windows: WinForms, WPF.
  • Multi-targeting. <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks> (у множині): проєкт збирається двічі, а dotnet pack кладе в пакет lib/netstandard2.0 і lib/net8.0.
  • Відмінності в коді — через символи NETSTANDARD2_0, NETFRAMEWORK, NET8_0_OR_GREATER та умовні PackageReference.

Коли що обирати

Бібліотека Цілі
Внутрішня, для одного застосунку Той самий TFM, що й у застосунку
Публічна, потрібен .NET Framework netstandard2.0 і net8.0
Публічна, лише сучасний .NET net8.0; netstandard2.1 Microsoft радить пропустити

Пастки

Проєкт на netstandard2.0 може посилатися на збірку .NET Framework (режим сумісності). Компіляція пройде, а під час виконання можливі TypeLoadException або MissingMethodException.

Правило

Одна ціль, доки немає реальних споживачів на іншій платформі.

Під капотом

  • Специфікація відставала від реалізації на роки: API з’являвся в .NET Core, потім потрапляв у стандарт, потім стандарт мали реалізувати всі платформи.
  • Платформозалежні API всередині стандарту кидали PlatformNotSupportedException уже під час виконання. У .NET 5+ специфікація й реалізація збігаються, а непідтримувані на ОС API ловить platform compatibility analyzer під час збирання.
  • З пакета з netstandard2.0 і net8.0 проєкт на net10.0 отримує net8.0 — найближчий сумісний TFM.

Ціна

  • Матриця CI: тести на кожній цілі, для net48 — Windows-агент.
  • Поліфіли (System.Memory, Microsoft.Bcl.AsyncInterfaces) та умовні залежності для старої цілі.
  • #if-гілки в коді.

Компроміси

Сучасна ціль дає перевантаження на Span<T>, nullable-анотації BCL та аналізатори сумісності з Native AOT — вони працюють лише для net8.0 і вище. Але публічний API в усіх цілях має збігатися. Інакше проміжна бібліотека, зібрана проти netstandard2.0-версії, впаде з MissingMethodException, коли застосунок на net8.0 підтягне іншу збірку.

Як перевірити

Package validation (EnablePackageValidation) під час dotnet pack перевіряє, що код, зібраний проти однієї цілі пакета, працює з іншими, а з PackageValidationBaselineVersion — ще й сумісність із попереднім релізом.

Підсумок: тримайте netstandard2.0, доки є живі споживачі на .NET Framework, і видаляйте ціль, коли останній мігрує.

Приклад
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<!-- Поліфіл потрібен лише старій цілі -->
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Memory" Version="4.5.5" />
</ItemGroup>
</Project>
public static class Guard
{
public static void NotNull(object? value, string name)
{
#if NET6_0_OR_GREATER
ArgumentNullException.ThrowIfNull(value, name); // API з .NET 6+
#else
if (value is null) throw new ArgumentNullException(name); // для netstandard2.0
#endif
}
}
Middle+

Що таке збірка і як NuGet розв’язує конфлікти версій залежностей?

Section titled “Що таке збірка і як NuGet розв’язує конфлікти версій залежностей?”
Відповідь

Що це

  • Збірка — результат компіляції проєкту: MyLib.dll або застосунок. Усередині IL-код, метадані й маніфест — ім’я, версія та список збірок, від яких вона залежить.
  • NuGet-пакет — архів .nupkg зі збірками під різні платформи (теки lib/net8.0, lib/netstandard2.0) і списком залежностей.

Як користуватися

  1. Додати пакет: dotnet add package Serilog — у .csproj з’явиться <PackageReference>.
  2. dotnet restore (його запускає й dotnet build) завантажує пакет і його залежності в спільний кеш ~/.nuget/packages.
  3. Під час збирання проєкт посилається на потрібні .dll із цього кешу.

Типова помилка

Скопіювати .dll у репозиторій і послатися на файл. Так губляться залежності цієї збірки, оновлення й перевірка вразливостей.

Як влаштовано

Збірка — одиниця розгортання й версіонування. Її ідентичність — просте ім’я, AssemblyVersion, культура й токен публічного ключа. Тип невіддільний від збірки: MyType із двох різних збірок — два різні типи.

Як NuGet обирає версію

Правило Що робить
Lowest applicable version >= 2.1 дає мінімальну доступну версію від 2.1
Direct dependency wins версія з проєкту перемагає транзитивну
Cousin dependencies >= 1.0 і >= 2.0 у різних гілках дають 2.0

Результат записується в obj/project.assets.json.

Пастки

  • Версія в проєкті нижча за ту, якої вимагає залежність, — це пониження, попередження NU1605. .NET SDK вважає його помилкою.
  • Точні версії [1.0] і [2.0] у різних гілках не поєднати — помилка NU1107.

Правило

Конфлікт розв’язують прямим посиланням на обрану версію, зазвичай на вищу. Хто притягнув пакет, показують dotnet nuget why <пакет> (SDK 8.0.4xx+) і dotnet list package --include-transitive.

Під капотом

  1. NuGet під час restore обирає версію пакета.
  2. SDK записує шлях до збірки в app.deps.json.
  3. AssemblyLoadContext.Default завантажує збірку за простим ім’ям. Строге ім’я в .NET не перевіряється й у прив’язуванні не бере участі.
  4. Завантажена версія має бути не нижчою за ту, проти якої компілювали, інакше — FileLoadException.

Ціна

  • Мінімальна версія заради відтворюваності означає, що вразливі транзитивні версії «застрягають». NuGet Audit попереджає про них під час restore.
  • NuGet перевіряє діапазони версій, а не бінарну сумісність. Пакет A зібрано проти Lib 1.x, граф обрав Lib 2.0 з breaking change — збирання проходить, а під час виконання MissingMethodException.

Компроміси

  • Central Package Management задає версії в одному Directory.Packages.props. Transitive pinning (CentralPackageTransitivePinningEnabled) піднімає й транзитивні залежності, але понижувати їх не дає (NU1109).
  • У .NET Framework розбіжності версій закривають binding redirects в app.config (AutoGenerateBindingRedirects); без них — FileLoadException.
  • Дві версії однієї бібліотеки в процесі можливі лише в окремих AssemblyLoadContext, як у плагінів.

Як перевірити в продакшені

  • dotnet nuget why і obj/project.assets.json — звідки взялася версія.
  • DOTNET_HOST_TRACE=1 — які файли хост включив до списку збірок.
  • dotnet-dump або AppDomain.CurrentDomain.GetAssemblies() у діагностичному ендпоінті — що насправді завантажено.

Підсумок: версія обирається під час restore і фіксується в deps.json, а несумісність версій спливає лише під час виконання — її ловлять тестами.

Приклад
<!-- Directory.Packages.props у корені репозиторію -->
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<!-- Піднімати до цих версій і транзитивні залежності -->
<CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Serilog" Version="4.0.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
</ItemGroup>
</Project>
Окно терминала
dotnet list package --include-transitive # увесь граф із версіями
dotnet nuget why System.Text.Json # хто притягнув пакет у проєкт
Middle+

Що нового в .NET 8–10 і C# 12–14?

Section titled “Що нового в .NET 8–10 і C# 12–14?”
Відповідь

Що це

Кожен реліз .NET виходить зі своєю версією C#, і TFM визначає її за замовчуванням: net8.0 — C# 12, net9.0 — C# 13, net10.0 — C# 14.

Як користуватися

Версія Що з’явилося Приклад
C# 12 primary constructor, collection expressions class Service(ILogger<Service> log), int[] a = [1, 2, 3];
C# 13 params для будь-яких колекцій, тип Lock void Log(params ReadOnlySpan<string> parts)
C# 14 field, ?. ліворуч від присвоєння customer?.Order = order;
.NET 10 SDK запуск одного файлу без проєкту dotnet run app.cs

Типова помилка

Підняти LangVersion у старому проєкті замість переходу на новий .NET. Частина фіч потребує типів із нової BCL, і така конфігурація офіційно не підтримується.

Що з’явилося

Реліз Платформа Мова
.NET 8, LTS Native AOT для ASP.NET Core, keyed DI, TimeProvider, FrozenDictionary C# 12: primary constructors, collection expressions
.NET 9, STS Task.WhenEach, CountBy, Guid.CreateVersion7, видалення BinaryFormatter C# 13: params-колекції, Lock
.NET 10, LTS file-based apps, OpenAPI 3.1, валідація в minimal API C# 14: field, extension members, ?. у присвоєнні

Про що питають детальніше

  • Keyed DI. AddKeyedSingleton<ICache, BigCache>("big") і [FromKeyedServices("big")] — кілька реалізацій одного інтерфейсу за ключем.
  • TimeProvider — абстракція поточного часу й таймерів, щоб тестувати код, який залежить від часу.
  • System.Threading.Lock. lock за таким об’єктом компілюється у виклики його API, а не Monitor.
  • File-based apps. Директиви #:package і #:project на початку .cs-файлу; публікація за замовчуванням у Native AOT.

Пастки

  • BinaryFormatter у .NET 9 завжди кидає виняток, і легасі-серіалізація на ньому ламається.
  • Параметри primary constructor не readonly: їх можна випадково перепризначити.

Правило

Читати список breaking changes до апгрейду й переходити з LTS на наступну LTS не пізніше кінця підтримки.

Під капотом

  • Dynamic PGO (.NET 8, за замовчуванням): JIT збирає профіль гарячих шляхів і типів під час роботи й використовує його під час перекомпіляції. За даними Microsoft — у середньому близько +15% продуктивності.
  • DATAS (.NET 9, за замовчуванням при Server GC): GC підлаштовує кількість куп і розмір купи під навантаження, а не під кількість ядер.
  • Escape analysis (.NET 10): об’єкти, які не залишають метод, — маленькі масиви, делегати — JIT розміщує на стеку.

Ціна

DATAS зменшує пам’ять ціною частини пропускної здатності. У бенчмарках Microsoft на 48 ядрах — мінус 2–3% RPS при working set, меншому більш ніж на 80%. Якщо важливіший RPS, його вимикають: DOTNET_GCDynamicAdaptationMode=0.

Компроміси

  • Primary constructors. Параметр, використаний у методах чи властивостях, стає прихованим змінюваним полем: зручно для DI, погано для інваріантів.
  • Collection expressions. IEnumerable<int> x = [1, 2]; дає згенерований компілятором read-only тип; приведення до List<int> кине InvalidCastException.
  • params ReadOnlySpan<T> прибирає алокацію масиву, але нові перевантаження BCL під час перекомпіляції можуть змінити обраний метод. Для керування цим є OverloadResolutionPriority.
  • field затіняє поле з іменем field усередині аксесорів; потрібен @field або this.field.

Як перевірити в продакшені

До перемикання трафіку — dotnet-counters (GC, пам’ять, JIT) і навантажувальний тест на старій і новій версії, BenchmarkDotNet — на критичних шляхах.

Підсумок: апгрейд .NET дає безкоштовну продуктивність зі зміненим профілем, і її підтверджують вимірюваннями.

Приклад
using Microsoft.Extensions.Logging;
// C# 12: primary constructor і collection expression
public class OrderService(ILogger<OrderService> logger)
{
private readonly List<string> _tags = ["new"];
// C# 13: params-колекція без алокації масиву
public void AddTags(params ReadOnlySpan<string> tags)
{
foreach (var tag in tags) _tags.Add(tag);
logger.LogInformation("Тегів: {Count}", _tags.Count);
}
// C# 14: field замість явного поля
public string Name { get; set => field = value.Trim(); } = "";
}
// C# 14: extension-властивість
public static class StringExtensions
{
extension(string s)
{
public bool IsBlank => string.IsNullOrWhiteSpace(s);
}
}
Senior+

Що таке tiered compilation, ReadyToRun і Native AOT і коли що застосовувати?

Section titled “Що таке tiered compilation, ReadyToRun і Native AOT і коли що застосовувати?”
Відповідь

Що це

  • Tiered compilation — JIT спочатку швидко компілює метод без оптимізацій, а методи, які викликаються часто, потім перекомпільовує з оптимізаціями. Увімкнена за замовчуванням з .NET Core 3.0.
  • ReadyToRun — машинний код додається в збірки під час публікації, JIT залишається.
  • Native AOT — увесь застосунок компілюється в один нативний файл; JIT і встановлений .NET не потрібні.

Як користуватися

  1. ReadyToRun: dotnet publish -c Release -r linux-x64 -p:PublishReadyToRun=true.
  2. Native AOT: <PublishAot>true</PublishAot> у .csproj і dotnet publish -c Release -r linux-x64.

Типова помилка

Ігнорувати попередження під час AOT-публікації: код із рефлексією збереться, але впаде вже під час роботи.

Як влаштовано

  1. Tier 0. Швидкий JIT без оптимізацій або готовий ReadyToRun-код.
  2. Підрахунок викликів починається, коли 100 мс поспіль не було нової tier-0-компіляції.
  3. Dynamic PGO (за замовчуванням з .NET 8). Гарячий метод проходить через інструментовану версію, яка збирає профіль: які типи приходять у віртуальні виклики, які гілки гарячі.
  4. Tier 1. Метод, викликаний 30 разів, у фоні перекомпільовується з оптимізаціями й профілем: девіртуалізація, інлайнінг, розкладка коду.
  5. OSR (з .NET 7 на x64 і Arm64). Метод із довгим циклом, викликаний один раз, перемикається на оптимізований код посеред циклу.

Коли що обирати

Ситуація Варіант
Звичайний довгоживучий сервіс JIT з налаштуваннями за замовчуванням
Важливий холодний старт, багато коду, є рефлексія ReadyToRun
Мінімальні старт і пам’ять, немає динаміки Native AOT

Пастки

[MethodImpl(MethodImplOptions.AggressiveOptimization)] одразу дає оптимізований код, але вимикає для методу tiering і Dynamic PGO.

Правило

Не чіпати налаштування tiering без вимірювань: значення за замовчуванням підходять майже всім.

Під капотом

  • ReadyToRun-код оптимізований гірше за JIT: він не знає точного процесора й обмежено інлайнить між збірками. Тому гарячі методи все одно йдуть у tier 1.
  • Native AOT під час публікації обрізає застосунок і компілює IL усіх збірок, що залишилися. У бінарнику — урізане середовище виконання без JIT.

Ціна

  • Native AOT. Немає Assembly.LoadFile, Reflection.Emit, C++/CLI і вбудованого COM; System.Linq.Expressions лише інтерпретуються. Generic-інстанціації зі структурами генеруються заздалегідь і роздувають бінарник. В ASP.NET Core не підтримуються MVC, Blazor Server і сесії.
  • Збирання AOT. Під конкретну ОС і архітектуру (RID) нативним тулчейном: MSVC або clang. Бінарник, зібраний на Linux, працює на тій самій або новішій версії дистрибутива.
  • ReadyToRun. Збірки у 2–3 рази більші; composite-режим дає кращий старт, але ще сильніше збільшує розмір.

Компроміси

AOT-код не переоптимізується під реальне навантаження, тому на довгоживучому сервісі пікова пропускна здатність у JIT із Dynamic PGO зазвичай не нижча. Якщо обрано AOT — з першого дня: PublishAot у проєкті (аналізатори працюють уже під час збирання), JSON через source generator, DI без сканування збірок, бібліотеки з IsAotCompatible.

Як перевірити в продакшені

Виміряти JIT, ReadyToRun і Native AOT: час до першої відповіді, working set, RPS після прогріву, розмір образу. dotnet-counters показує час у JIT, а для AOT-бінарника потрібні символи (.pdb або .dbg), інакше дампи не розібрати.

Підсумок: ReadyToRun — дешевий виграш старту, Native AOT — сильний виграш ціною обмежень на весь код, і вибір підтверджують вимірюваннями.

Приклад
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<!-- Варіант 1: ReadyToRun — швидший старт, JIT залишається -->
<PublishReadyToRun>true</PublishReadyToRun>
<!-- Варіант 2: Native AOT замість ReadyToRun — без JIT -->
<!-- <PublishAot>true</PublishAot> -->
</PropertyGroup>
</Project>
Окно терминала
dotnet publish -c Release -r linux-x64 # обидва варіанти потребують RID
Код, сумісний із Native AOT
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
// true з JIT, false у Native AOT: генерації коду під час роботи немає
Console.WriteLine(RuntimeFeature.IsDynamicCodeSupported);
var order = new Order(1, 99.5m);
// Серіалізатор згенеровано під час компіляції — рефлексія не потрібна
Console.WriteLine(JsonSerializer.Serialize(order, AppJson.Default.Order));
// {"Id":1,"Total":99.5}
record Order(int Id, decimal Total);
[JsonSerializable(typeof(Order))]
partial class AppJson : JsonSerializerContext { }