Платформа .NET
З чого складається платформа .NET і як код на C# стає працюючою програмою: версії та їхня підтримка, CLR і JIT, збірки й пакети, способи компіляції. Це фундамент для всіх інших тем розділу.
Чим .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,dotnetCLI і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 — остання версія, яка його підтримує).
Компроміси
Міграція «великим вибухом» ризикована, тому йдуть кроками:
- Перевести проєкти на SDK-style і
PackageReference. - Спільні бібліотеки зібрати під
netstandard2.0або одразу під обидві платформи. - Знайти несумісні API: platform compatibility analyzer, .NET Upgrade Assistant.
- Переносити хости. Веб — поступово: новий застосунок 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 NETFRAMEWORKConsole.WriteLine("Скомпільовано під .NET Framework");#elif NET5_0_OR_GREATERConsole.WriteLine("Скомпільовано під .NET 5+");#endif- Чому .NET 5, а не .NET Core 4? — Щоб не плутати з .NET Framework 4.x і показати, що це головна гілка.
- Чи можна запустити застосунок на .NET 10 без встановленого середовища виконання? — Так, через self-contained-публікацію або Native AOT.
- Чим замінити AppDomain під час міграції? — Ізоляцію — процесами чи контейнерами, завантаження плагінів —
AssemblyLoadContext. - Як одну бібліотеку використовувати і у Framework, і в .NET? — Зібрати її під
netstandard2.0або одразу під обидві платформи. - Чому після міграції рядки сортуються інакше? — .NET 5+ використовує для глобалізації ICU, а не NLS.
Що таке 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 і далі) встановлюються без зміни проєкту.
Типова помилка
Залишатися на версії без підтримки, «бо працює».
Як влаштовано
- Щолистопада виходить major-версія; треки LTS і STS чергуються.
- Майже щомісяця виходить патч. Виправлення безпеки — у другий вівторок місяця (Patch Tuesday).
- Застосунок без вбудованого середовища виконання запускається на останньому встановленому патчі своєї версії. На нову major-версію він не переходить, доки не задано
RollForward(MajorабоLatestMajor). - 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 # встановлені SDKdotnet --list-runtimes # встановлені середовища виконанняdotnet sdk check # що застаріло і що вийшло з підтримки- Чим LTS відрізняється від STS за якістю? — Нічим: обидва готові до продакшену, різниця лише в строку підтримки.
- Що буде із застосунком на .NET 8 після 10 листопада 2026? — Працюватиме далі, але перестане отримувати виправлення безпеки.
- Чи запуститься застосунок під
net8.0, якщо встановлено лише .NET 10? — За замовчуванням ні; потрібенRollForwardзі значеннямMajorабоLatestMajor. - Навіщо
global.json? — Зафіксувати версію SDK, щоб збирання було однаковим у всіх розробників і в CI. - Чому у 2025 році STS подовжили до 24 місяців? — Щоб пакети зі STS-релізів не скорочували підтримку LTS-застосунків.
Як виконується код на C#: IL, CLR, JIT?
Section titled “Як виконується код на C#: IL, CLR, JIT?”Що це
- IL — проміжна мова. Компілятор C# записує її в збірку
.dllразом із метаданими — описом типів і методів. - CLR — середовище виконання: завантажує збірки, керує пам’яттю, винятками й потоками.
- JIT — частина CLR, яка перекладає IL методу в машинний код.
Як це працює
dotnet build— Roslyn компілює.cs-файли в IL.dotnet run— запускається CLR і завантажує збірку.- Перед першим викликом методу JIT компілює його в код для поточного процесора.
- Наступні виклики йдуть одразу в готовий машинний код.
Типова помилка
Думати, що C# інтерпретується, як Python. Процесор завжди виконує машинний код, просто цей код з’являється під час роботи програми.
Як влаштовано
- Запускач (
dotnet app.dllабоapp.exe) знаходитьhostfxr. Той читаєapp.runtimeconfig.jsonі обирає версію середовища виконання. hostpolicyчитаєapp.deps.json, складає список збірок застосунку й запускає CoreCLR — CLR сучасного .NET.- CLR завантажує збірку й знаходить точку входу.
- Кожен метод спочатку викликається через заглушку. Вона запускає JIT і підміняє себе адресою готового коду.
- Метод, який жодного разу не викликали, не компілюється ніколи.
Що ще дає 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- Що таке метадані й навіщо вони? — Опис типів і членів усередині збірки; на них працюють рефлексія, серіалізація й перевірка типів.
- Чи компілюється вся програма під час старту? — Ні, метод компілюється під час першого виклику, невикликаний — ніколи.
- Чим CLR відрізняється від CoreCLR? — CLR — середовище виконання .NET Framework, CoreCLR — сучасного .NET.
- Чи можна в одному рішенні писати на C# і F#? — Так, обидві мови компілюються в IL і використовують спільну систему типів.
- Чому перший запит до сервісу повільний? — JIT і завантаження типів; допомагають tiered compilation, ReadyToRun і прогрів.
Що таке .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 }}- Чим
netstandard2.0кращий заnetstandard2.1? — 2.0 підтримує .NET Framework, 2.1 — ні. - Чи можна зробити застосунок під .NET Standard? — Ні, це лише контракт для бібліотек.
- Як NuGet обирає збірку з пакета з кількома TFM? — Бере найближчий TFM, сумісний із проєктом.
- Що таке
net10.0-windows? — TFM для конкретної ОС: спільний API плюс технології лише для Windows. - Навіщо бібліотеці ціль
net8.0, якщо вже єnetstandard2.0? — Нові API, nullable-анотації BCL, аналізатори сумісності з Native AOT.
Що таке збірка і як NuGet розв’язує конфлікти версій залежностей?
Section titled “Що таке збірка і як NuGet розв’язує конфлікти версій залежностей?”Що це
- Збірка — результат компіляції проєкту:
MyLib.dllабо застосунок. Усередині IL-код, метадані й маніфест — ім’я, версія та список збірок, від яких вона залежить. - NuGet-пакет — архів
.nupkgзі збірками під різні платформи (текиlib/net8.0,lib/netstandard2.0) і списком залежностей.
Як користуватися
- Додати пакет:
dotnet add package Serilog— у.csprojз’явиться<PackageReference>. dotnet restore(його запускає йdotnet build) завантажує пакет і його залежності в спільний кеш~/.nuget/packages.- Під час збирання проєкт посилається на потрібні
.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.
Під капотом
- NuGet під час restore обирає версію пакета.
- SDK записує шлях до збірки в
app.deps.json. AssemblyLoadContext.Defaultзавантажує збірку за простим ім’ям. Строге ім’я в .NET не перевіряється й у прив’язуванні не бере участі.- Завантажена версія має бути не нижчою за ту, проти якої компілювали, інакше —
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 # хто притягнув пакет у проєкт- Чим
AssemblyVersionвідрізняється від версії пакета? — Версія пакета потрібна NuGet,AssemblyVersion— завантажувачу; вони можуть не збігатися. - Чому NuGet бере мінімальну версію, а не останню? — Заради відтворюваних збирань: граф не змінюється з виходом нових версій.
- Що означає NU1605? — Пониження версії: проєкт указав версію, нижчу за ту, якої вимагає залежність.
- Чи потрібні binding redirects у .NET 8? — Ні, це механізм .NET Framework; у .NET завантажується версія з
deps.json, якщо вона не нижча за запитану. - Як завантажити дві версії однієї бібліотеки? — Через окремі
AssemblyLoadContext, як це роблять плагіни.
Що нового в .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 expressionpublic 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); }}- Чим primary constructor класу відрізняється від
record? — У класу параметри не стають публічними властивостями, аrecordгенерує властивості, рівність іDeconstruct. - Що дає
System.Threading.Lockпорівняно зlockзаobject? — Окремий тип з APIEnterScope; компілятор генерує код через нього, а не черезMonitor. - Що таке DATAS? — Режим Server GC, який підлаштовує кількість і розмір куп під навантаження; увімкнений за замовчуванням з .NET 9.
- Чому видалили
BinaryFormatter? — Небезпечна десеріалізація; у .NET 9 вбудована реалізація завжди кидає виняток. - Чи можна використовувати C# 14 у проєкті під
net8.0? — Офіційно ні: версія мови прив’язана до TFM, а частина фіч потребує нової BCL.
Що таке 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 не потрібні.
Як користуватися
- ReadyToRun:
dotnet publish -c Release -r linux-x64 -p:PublishReadyToRun=true. - Native AOT:
<PublishAot>true</PublishAot>у.csprojіdotnet publish -c Release -r linux-x64.
Типова помилка
Ігнорувати попередження під час AOT-публікації: код із рефлексією збереться, але впаде вже під час роботи.
Як влаштовано
- Tier 0. Швидкий JIT без оптимізацій або готовий ReadyToRun-код.
- Підрахунок викликів починається, коли 100 мс поспіль не було нової tier-0-компіляції.
- Dynamic PGO (за замовчуванням з .NET 8). Гарячий метод проходить через інструментовану версію, яка збирає профіль: які типи приходять у віртуальні виклики, які гілки гарячі.
- Tier 1. Метод, викликаний 30 разів, у фоні перекомпільовується з оптимізаціями й профілем: девіртуалізація, інлайнінг, розкладка коду.
- 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 # обидва варіанти потребують RIDusing 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 { }- Чому гарячий метод не оптимізується одразу? — Чекає 30 викликів і паузи в tier-0-компіляції, щоб не гальмувати старт.
- Що буде з
Reflection.Emitу Native AOT? — Не підтримується:RuntimeFeature.IsDynamicCodeSupportedдорівнюєfalse. - Чим ReadyToRun відрізняється від Native AOT? — ReadyToRun додає машинний код до IL і залишає JIT, Native AOT прибирає JIT повністю.
- Навіщо потрібен OSR? — Щоб метод із довгим циклом, викликаний один раз, не застрягав у неоптимізованому коді.
- Коли Native AOT — поганий вибір? — Багато рефлексії, плагіни, MVC, генерація коду під час роботи, бібліотеки без підтримки AOT.