Posted on 10. December 2023

.NET 8 Networking Improvements

Покращення роботи з мережею в .NET 8

Вже стало традицією публікувати в блозі повідомлення про нові цікаві зміни в мережевому просторі з новим випуском .NET . Цього року ми хотіли б запровадити зміни в просторі HTTP , нові додані метрики, нові API HttpClientFactory, тощо.


HTTP

Метрики

.NET 8 додає вбудовані HTTP-метрики як до ASP.NET Core, так і до HttpClient, за допомогою System.Diagnostics.Metrics API, який був представлений у .NET 6. І API-інтерфейси Metrics, і семантика нових вбудованих показників були розроблені у тісній співпраці з OpenTelemetry, переконуючись, що нові показники відповідають стандарту та добре працюють із такими популярними інструментами, як Prometheus і Grafana. .

API System.Diagnostics.Metrics представляє багато нових функцій, яких не було в EventCounters. Ці функції широко використовуються новими вбудованими метриками, що призводить до ширшої функціональності, досягнутої простішим і елегантнішим набором інструментів. Наведу кілька прикладів:

Гістограми дозволяють нам повідомляти про тривалості, напр. тривалість запиту ( http.client.request.duration) або тривалість з’єднання (http.client.connection.duration). Це нові показники без відповідників EventCounter.

Багатовимірність дозволяє нам додавати теги (атрибути або мітки) до вимірювань, що означає, що ми можемо повідомляти таку інформацію, як server.address (ідентифікує джерело URI) або error.type (описує причину помилки, якщо запит не вдається) разом із вимірюваннями. Багатовимірність також забезпечує спрощення: для звітування про кількість відкритих HTTP-з’єднань SocketsHttpHandler використовує 3 EventCounters: http11-connections-current-total, http20-connections-current-totalіhttp30-connections-current-total , тоді як еквівалент цих лічильників Metrics є одним інструментом, http.client.open_connections, де версія HTTP повідомляється за допомогою тегу network.protocol.version.

Щоб допомогти використати випадки, коли вбудованих тегів недостатньо для класифікації вихідних HTTP-запитів, метрика http.client.request.duration підтримує впровадження тегів, визначених користувачем. Це називається збагаченням.

Інтеграція IMeterFactory дозволяє ізолювати екземпляри Meter, які використовуються для випромінювання метрик HTTP, що полегшує написання тестів, які перевіряють вбудовані вимірювання, і дозволяє паралельне виконання таких тестів.

– Хоча це не стосується вбудованих мережевих метрик, варто зазначити, що API колекції System.Diagnostics.Metrics також є більш досконалими: вони суворо типізовані та більш продуктивні та відкривають кільком слухачам одночасно доступ до неагрегованих вимірювань.

Ці переваги разом призводять до кращих, багатших показників, які можна ефективніше збирати сторонніми інструментами, такими як Prometheus. Завдяки гнучкості PromQL (Prometheus Query Language) , яка дозволяє створювати складні запити на основі багатовимірних показників, зібраних із мережевого стеку .NET, користувачі тепер можуть отримувати статистичні дані про стан і працездатність екземплярів HttpClient та SocketsHttpHandler на рівні, який не був раніше можливим.

З іншого боку, слід зазначити, що лише компоненти System.Net.Http та System.Net.NameResolution інструментуються за допомогою System.Diagnostics.Metrics у .NET 8, а це означає, що вам все одно потрібно використовувати EventCounters для отримання лічильників із нижчих рівнів стеку, таких як System.Net.Sockets. Хоча всі вбудовані лічильники подій, які існували в попередніх версіях, все ще підтримуються, команда .NET не очікує значних нових інвестицій у лічильники подій, і нові вбудовані інструменти будуть додані, використовуючи System.Diagnostics.Metrics, в майбутніх версіях.

Щоб отримати додаткові відомості про використання вбудованих метрик HTTP, прочитайте наш підручник щодо мережевих метрик у .NET . Він містить приклади збирання та звітування за допомогою Prometheus і Grafana, а також демонструє, як збагачувати та тестувати вбудовані HTTP-метрики. Щоб отримати вичерпний список вбудованих інструментів, перегляньте документацію для метрик System.Net . Якщо вас більше цікавить серверна сторона, будь ласка, прочитайте документацію про показники ASP.NET Core.

Розширена телеметрія

Окрім нових показників, наявні телеметричні події EventSource, представлені в .NET 5, були доповнені додатковою інформацією про HTTP-з’єднання ( dotnet/runtime#88853 ):

Тепер, коли встановлюється нове з’єднання, подія логує connectionId разом із схемою, портом та IP-адресою однорангового пристрою. Це дає змогу співвідносити запити та відповіді зі з’єднаннями через подію RequestHeadersStart, яка виникає, коли запит пов’язується з об’єднаним з’єднанням і починає оброблятися, яка також реєструє пов’язані connectionId. Це особливо цінно в діагностичних сценаріях, коли користувачі хочуть бачити IP-адреси серверів, які обслуговують їхні HTTP-запити, що було основною мотивацією додавання ( dotnet/runtime#63159 ).

 

Події можна використовувати багатьма способами, див. Мережева телеметрія в .NET – Події . Але для покращеного журналювання під час процесу EventListener можна використовувати спеціальний параметр, щоб співвіднести пару запит/відповідь із даними підключення:

Крім того, подію Redirect було розширено, щоб включити URI перенаправлення:

-void Redirect();

 

+void Redirect(string redirectUri);

Коди помилок HTTP

Одна з проблем діагностики HttpClient полягала в тому, що у випадку винятку було непросто програмно визначити точну причину помилки. Єдиним способом відрізнити багато з них було розібрати повідомлення про винятки з HttpRequestException. Крім того, інші реалізації HTTP, такі як WinHTTP із кодами помилок ERROR_WINHTTP_*, пропонують такі функції у формі числових кодів або перерахувань. Отже, .NET 8 представляє подібний перелік і надає його у винятках, створених обробкою HTTP, які є:

HttpRequestException для обробки запиту до отримання заголовків відповіді.

HttpIOException для читання змісту відповіді.

Дизайн HttpRequestError enum і те, як він підключається до винятків HTTP, описано в пропозиції API dotnet/runtime#76644 .


Тепер споживач методів HttpClient може обробляти конкретні внутрішні помилки набагато легше та надійніше:

Підтримка проксі HTTPS

Однією з особливо затребуваних функцій, яку було реалізовано в цьому випуску, є підтримка HTTPS-проксі ( dotnet/runtime#31113). Тепер можна використовувати проксі, які обслуговують запити через HTTPS, тобто з’єднання з проксі є безпечним. Це нічого не говорить про сам запит від проксі, який може бути як HTTP, так і HTTPS. У випадку звичайного текстового HTTP-запиту з’єднання з проксі-сервером HTTPS є безпечним (через HTTPS), а потім іде простий текстовий запит від проксі-сервера до пункту призначення. У разі запиту HTTPS (тунель проксі) початковий запит CONNECT на відкриття тунелю буде надіслано через захищений канал (HTTPS) до проксі, а потім запит HTTPS від проксі до пункту призначення через тунель.

 

Щоб скористатися цією функцією, все, що потрібно, це використовувати схему HTTPS під час налаштування проксі:

HttpClientFactory

.NET 8 розширює можливості налаштування HttpClientFactory, включаючи параметри клієнта за замовчуванням, спеціальне логування та спрощену конфігурацію SocketsHttpHandler. API реалізовано в пакеті Microsoft.Extensions.Http, який доступний на NuGet і включає підтримку .NET Standard 2.0. Таким чином, цю функцію можна використовувати клієнтам не лише в .NET 8, але й у всіх версіях .NET, включаючи .NET Framework (єдиним винятком є ​​відповідні API SocketsHttpHandler, які доступні лише для .NET 5+).

Налаштуйте параметри за замовчуванням для всіх клієнтів

.NET 8 додає можливість установити конфігурацію за замовчуванням, яка використовуватиметься для всіх HttpClient-ів створених HttpClientFactory( dotnet/runtime#87914). Це корисно, коли всі або більшість зареєстрованих клієнтів містять однакову підмножину конфігурації.

 

Розглянемо приклад, де визначено два клієнти з іменами, і обидва вони потребують MyAuthHandler для свого ланцюжка обробників повідомлень.

 

Щоб витягти загальну частину, тепер ви можете використовувати метод ConfigureHttpClientDefaults:

Усі методи розширення IHttpClientBuilder, які використовуються з AddHttpClient також можна використовувати всередині .ConfigureHttpClientDefaults.

Конфігурація за замовчуванням (ConfigureHttpClientDefaults) застосовується до всіх клієнтів перед конфігураціями для конкретного клієнта (AddHttpClient); їх відносне положення в реєстрації не має значення. ConfigureHttpClientDefaults можна зареєструвати кілька разів, у цьому випадку конфігурації будуть застосовані одна за одною в порядку реєстрації. Будь-яка частина конфігурації може бути перевизначена або змінена в конфігураціях для конкретного клієнта, наприклад, ви можете встановити додаткові параметри для об’єкта HttpClient або основного обробника, видалити раніше доданий додатковий обробник тощо.

 

Зауважте, що з версії 8.0 метод ConfigureHttpMessageHandlerBuilder застарів . Натомість вам слід використовувати методи ConfigurePrimaryHttpMessageHandler(Action))) або ConfigureAdditionalHttpMessageHandlers, щоб змінити попередньо налаштований основний обробник або список додаткових обробників відповідно.

Змінити логування HttpClient

Налаштування (або навіть просто вимкнення) логування  HttpClientFactory було однією з давно запитуваних функцій (dotnet/runtime#77312).

Огляд старого логування

Логування за замовчуванням («старе»), додане HttpClientFactory досить багатослівне і видає 8 повідомлень журналу на запит:

1. Почати сповіщення з URI запиту — перед розповсюдженням через конвеєр обробника делегування;

2. Заголовки запитів — перед пайплайном обробника;

3. Почати сповіщення з URI запиту — після пайплайну обробника;

4. Заголовки запитів — після пайплайну обробника;

5. Зупинити сповіщення з вичерпаним часом — перед розповсюдженням відповіді через конвеєр обробника делегування;

6. Заголовки відповіді — перед тим, як передавати відповідь назад;

7. Зупинити сповіщення з вичерпаним часом — після повернення відповіді;

8. Заголовки відповіді — після передачі відповіді назад.

 

Це можна проілюструвати схемою нижче. На цій і наступних діаграмах *і […]позначає подію логування (у реалізації за замовчуванням повідомлення журналу записується в ILogger), і символізує потік даних через прикладний і транспортний рівні.

Консольний вивід логу за замовчуванням HttpClientFactory виглядає так:

Зауважте, що для перегляду повідомлень рівня Trace вам потрібно ввімкнути це у файлі конфігурації глобального логування або за допомогою SetMinimumLevel(LogLevel.Trace). Але навіть враховуючи лише Informational повідомлення, «старе» логування все одно має 4 повідомлення на запит.

 

Щоб видалити стандартне (або раніше додане) логування, ви можете використати новий метод розширення RemoveAllLoggers(). Він особливо потужний у поєднанні з API ConfigureHttpClientDefaults, описаним у розділі «Налаштування параметрів за замовчуванням для всіх клієнтів» вище. Таким чином ви можете видалити «старе» журналювання для всіх клієнтів одним рядком:

Якщо вам колись знадобиться повернути «старе» логування, наприклад, для певного клієнта, ви можете зробити це за допомогою AddDefaultLogger().


Додати спеціальне логування

Окрім можливості видаляти «старе» логування, нові API HttpClientFactory також дозволяють повністю налаштувати логування. Ви можете вказати, що та як реєструватиметься, коли HttpClient починає запит, отримує відповідь або створює виняток.

Ви можете додати кілька власних реєстраторів разом, якщо ви вирішите зробити це, наприклад, консольні та ETW-реєстратори або обидва «пакетні» та «непакетовані» журнали. Через його адитивну природу вам може знадобитися явно видалити «старе» логування за замовчуванням заздалегідь.


Щоб додати настроюване ведення журналу, вам потрібно реалізувати інтерфейс IHttpClientLogger, а потім додати настроюваний реєстратор до клієнта за допомогою AddLogger. Зауважте, що реалізація логування не повинна створювати жодних винятків, інакше це може порушити виконання запиту.

 

Реєстрація:

Приклад імплементації логера:

Зразок результату:

Об’єкт контексту запиту

Контекстний об’єкт можна використовувати для зіставлення виклику LogRequestStart з відповідним викликом LogRequestStop для передачі даних від одного до іншого. Контекстний об’єкт створюється, LogRequestStart, а потім повертається до LogRequestStop. Це може бути сумка майна або будь-який інший об’єкт, який містить необхідні дані.

Якщо об’єкт контексту не потрібен, імплементація може повернути null з LogRequestStart.

 

У наступному прикладі показано, як об’єкт контексту можна використовувати для передачі ідентифікатора спеціального запиту.

Уникайте читання з потоків вмісту

Якщо ви збираєтеся читати та логувати,  наприклад, вміст запиту та відповіді, майте на увазі, що це потенційно може мати несприятливий побічний ефект для роботи кінцевого користувача та викликати помилки. Наприклад, вміст запиту може бути використано до того, як його буде надіслано, або вміст відповіді величезного розміру може бути буферизовано в пам’яті. Крім того, до .NET 7 доступ до заголовків не був потоково безпечним і міг призвести до помилок і неочікуваної поведінки.

Використовуйте асинхронне логування з обережністю

Ми очікуємо, що синхронний інтерфейс IHttpClientLogger підійде для переважної більшості випадків використання спеціального логування. Рекомендується утримуватися від використання асинхронного протоколу з міркувань продуктивності. Однак, якщо суворо потрібен асинхронний доступ до логування, ви можете застосувати асинхронну версію IHttpClientAsyncLogger. Він походить від IHttpClientLogger, тому може використовувати той самий API AddLogger для реєстрації.

Зауважте, що в такому випадку слід також реалізувати аналоги синхронізації методів логування, особливо якщо реалізація є частиною бібліотеки, орієнтованої на .NET Standard або .NET 5+. Відповідники синхронізації викликаються з методів синхронізації HttpClient.Send; навіть якщо поверхня .NET Standard їх не містить, бібліотеку .NET Standard можна використовувати в програмі .NET 5+, щоб кінцеві користувачі мали доступ до методів синхронізації HttpClient.Send.

Обгортання та не обгортання логерів

Коли ви додаєте логер, ви можете явно встановити параметр wrapHandlersPipeline, щоб вказати, чи буде логер

 

– обгортати пайплайн обробників (додано до верхньої частини пайплайну, що відповідає повідомленням № 1, 2, 7 і 8 у розділі «Огляд старого логування» вище)

 

або не обгортати пайплайн обробників (додано внизу, що відповідає повідомленням № 3, 4, 5 і 6 у розділі «Огляд старого логування» вище).

За замовчуванням логери додаються як не обгортаючі.


Різниця між обгортанням і не обгортанням пайплайну є найбільш помітною у випадку додавання до пайплайну обробника повторних спроб (наприклад, Polly або іншої користувацької реалізації повторних спроб). У цьому випадку логер з обгортанням (у верхній частині) буде реєструвати повідомлення про один успішний запит, а час, що минув, буде загальним часом від моменту, коли користувач ініціював запит, до моменту отримання ним відповіді. Логер без обгортання (внизу) реєстрував би кожну ітерацію повторної спроби, причому перші ітерації могли б містити виняток або код невдалої спроби, а остання – успішний результат. Час, що минув у кожному випадку, був би часом, витраченим виключно на первинний обробник (той, що фактично надсилає запит по дроту, наприклад, HttpClientHandler)).

Це можна проілюструвати наступними діаграмами:


– Кейс з обгортанням ( wrapHandlersPipeline=TRUE)

Кейс без обгортання (wrapHandlersPipeline=FALSE)

Спрощена конфігурація SocketsHttpHandler

.NET 8 додає більш зручний і плавний спосіб використання SocketsHttpHandler як основного обробника в HttpClientFactory (dotnet/runtime#84075)

Ви можете встановити та налаштувати SocketsHttpHandler за допомогою методу UseSocketsHttpHandler. Ви можете використовувати IConfiguration для встановлення властивостей SocketsHttpHandler із конфігураційного файлу, або ви можете налаштувати його з коду, або ви можете комбінувати обидва підходи.

 

Зауважте, що під час застосування IConfiguration до SocketsHttpHandler аналізуються лише властивості SocketsHttpHandler типу bool, int, Enum або TimeSpan . Усі невідповідні властивості в IConfiguration ігноруються . Конфігурація аналізується лише один раз після реєстрації та не перезавантажується, тому обробник не відображатиме жодних змін конфігураційного файлу, доки програму не буде перезапущено.

}

QUIC

Підтримка OpenSSL 3

Більшість поточних дистрибутивів Linux прийняли OpenSSL 3 у своїх останніх випусках:

– Debian 12+: Bookworm OpenSSL

– Ubuntu 22+: Jammy OpenSSL

– Fedora 37+: Fedora OpenSSL

– OpenSUSE: Tumbleweed OpenSSL

– AlmaLinux 9+: репозиторій пакетів AlmaLinux 9


Підтримка QUIC .NET 8 готова до цього (dotnet/runtime#81801).


Першим кроком для досягнення цього було переконатися, що MsQuic, реалізація QUIC, яка використовується нижче System.Net.Quic, може працювати з OpenSSL 3+. Ця робота відбулася в репозиторії MsQuic microsoft/msquic#2039. Наступним кроком було переконатися, що пакет libmsquic створено та опубліковано з відповідною залежністю від версії OpenSSL за замовчуванням для конкретного дистрибутива та версії. Наприклад, дистрибутив Debian:

– Debian 11 libmsquic залежить від OpenSSL 1.1

– Debian 12 libmsquic залежить від OpenSSL 3


Останнім кроком було переконатися, що тестуються правильні версії MsQuic і OpenSSL і що тести охоплюють усі дистрибутиви, що підтримуються .NET.


Винятки

Після публікації QUIC API у .NET 7 (як функція попереднього перегляду) ми отримали кілька проблем щодо винятків:

dotnet/runtime#78751 : QuicConnection.ConnectAsync генерує виключення SocketException, коли хост не знайдено

dotnet/runtime#78096: QuicListener AcceptConnectionAsync і OperationCanceledException

dotnet/runtime#75115: QuicListener.AcceptConnectionAsync повторне виключення


У .NET 8 поведінку винятків System.Net.Quic було повністю переглянуто в dotnet/runtime#82262 і вирішено вищезгадані проблеми.

Одна з головних цілей перегляду полягала в тому, щоб переконатися, що поведінка винятків у System.Net.Quic є максимально узгодженою в усьому просторі імен. Загалом поточну поведінку можна підсумувати таким чином:

QuicException: усі помилки, характерні для протоколу QUIC або пов’язані з його обробкою.

- З’єднання закрито локально або одноранговим вузлом.

- Підключення перервано через бездіяльність.

- Потік перервано локально або партнером.

- Інші помилки, описані в QuicError


SocketException: для мережевих проблем, таких як умови мережі, розпізнавання імен або помилки користувача.

- Адреса вже використовується.

- Цільовий хост не доступний.

- Вказана адреса недійсна.

- Неможливо визначити ім’я хоста.


AuthenticationException: для всіх питань, пов’язаних із TLS. Мета полягає в тому, щоб мати таку ж поведінку, як SslStream.

- Помилки, пов’язані із сертифікатом.

- Помилки узгодження ALPN.

- Скасування користувачем під час рукостискання.


ArgumentException: коли надані  QuicConnectionOptions або QuicListenerOptions недійсні.

- Надані обмеження потоку не входять у діапазон 0-65535.

- Пропущення обов’язкових властивостей, таких як: DefaultCloseErrorCode або DefaultStreamErrorCode.

- Невизначеність ClientAuthenticationOptions або ServerAuthenticationOptions.

 

OperationCanceledException: щоразу коли CancellationToken отримує скасування.

ObjectDisposedException: кожного разу, коли викликається метод на вже видаленому об’єкті.

Зауважте, що наведені вище приклади не є вичерпними.


Крім зміни поведінки, змінилася також QuicException. Однією з цих змін було коригування значень enum QuicError. Елементи, які зараз охоплюються, SocketException було видалено, і додано нове значення для помилок зворотного виклику користувача (dotnet/runtime#87259). Нещодавно доданий CallbackError використовується для того, щоб відрізнити винятки, створені, QuicListenerOptions.ConnectionOptionsCallbac k від System.Net.Quic ( dotnet/runtime#88614). Отже, якщо користувацький код згенерує, наприклад, ArgumentException, QuicListener.AcceptConnectionAsync оберне його у QuicException  з QuicError , встановленим у CallbackError, а внутрішній виняток міститиме оригінальний виняток користувача. Це можна використовувати таким чином:

Останньою зміною в просторі винятків було додавання коду транспортної помилки до QuicException ( dotnet/runtime#88550). Коди транспортних помилок визначені RFC 9000 Коди транспортних помилок, і вони вже були доступні в System.Net.Quic MsQuic , просто вони не були оприлюднені. Отже, до QuicException: TransportErrorCode була додана нова властивість, яка може бути скасована. Ми хотіли б подякувати учаснику спільноти AlexRadch, який вніс цю зміну в dotnet/runtime#88614.

Сокети

Найвпливовішою зміною, зробленою в просторі сокетів, було значне зменшення розподілу для сокетів без підключення (UDP) (dotnet/runtime#30797). Одним із найбільших факторів розподілу під час роботи з UDP-сокетами було виділення нового об’єкта EndPoint (і підтримка виділень, таких як IPAddress) під час кожного виклику Socket.ReceiveFrom. Щоб пом’якшити це, натомість було введено набір нових API, які працюють із SocketAddress( dotnet/runtime#87397). SocketAddress внутрішньо зберігає IP-адресу як масив байтів у формі, що залежить від платформи, щоб її можна було безпосередньо передати до викликів операційної системи. Таким чином, жодних копій даних IP-адреси не потрібно робити перед викликом функцій власного сокета.

 

Крім того, нещодавно додані перевантаження ReceiveFrom-system-net-sockets-socketflags-system-net-socketaddress)) і ReceiveFromAsync-system-net-sockets-socketflags-system-net-socketaddress-system-threading-cancellationtoken)) не створюють новий екземпляр IPEndPoint під час кожного виклику, але радше змінюють наданий  параметр receivedAddress на місці. Усе це разом можна використати, щоб зробити код сокета UDP більш ефективним:

Крім того, було покращено роботу з SocketAddress dotnet/runtime#86872 . SocketAddress тепер має кілька додаткових учасників, які роблять його більш корисним самостійно:

– getter Buffer: для доступу до всього основного буфера адреси.

– setter Size: щоб мати можливість налаштувати вищезгаданий розмір буфера (тільки до меншого розміру).

– static GetMaximumAddressSize: щоб отримати необхідний розмір буфера на основі типу адреси.

interface IEquatable: SocketAddress можна використовувати для розрізнення однорангових пристроїв, з якими спілкується сокет, наприклад, як ключ у словнику (це не нова функція, вона просто робить її доступною через інтерфейс).

І, нарешті, деякі внутрішні копії даних IP-адреси були видалені, щоб підвищити продуктивність.

Мережеві примітиви

Типи MIME

Додавання відсутніх типів MIME було однією з найбільш популярних проблем у мережевому просторі (dotnet/runtime#1489). Це була здебільшого зміна, керована спільнотою, яка призвела до пропозиції API dotnet/runtime#85807 . Оскільки це доповнення потребувало проходження процесу перевірки API, необхідно було переконатися, що додані типи є релевантними та відповідають специфікації (IANA Media Types). За цю підготовчу роботу ми хотіли б подякувати учасникам спільноти Bilal-io та mmarinchenko .

IPNetwork

Ще одним новим доповненням до API в .NET 8 є новий тип IPNetwork (dotnet/runtime#79946). Структура дозволяє вказувати безкласові IP-підмережі, як визначено в RFC 4632. Наприклад:

127.0.0.0/8 для безкласового визначення, що відповідає підмережі класу А.

42.42.128.0/17 для безкласової підмережі з 2¹⁵ адресами.

2a01:110:8012::/100 для IPv6 підмережі з 2²⁸ адресами.

 

Новий API пропонує створення або з IPAddress та довжини префікса за допомогою конструктора, або шляхом розбору рядка за допомогою TryParse або Parse. Крім того, він дозволяє перевірити приналежність IPAddress до підмережі за допомогою методу Contains. Приклад використання може виглядати так:

Зауважте, що цей тип не слід плутати з класом Microsoft.AspNetCore.HttpOverrides.IPNetwork, який існував у ASP.NET Core з 1.0. Ми очікуємо, що API ASP.NET з часом перейдуть до нового типу System.Net.IPNetwork  (dotnet/aspnetcore#46157).


Завершальні примітки

Теми, вибрані для цієї публікації в блозі, не є вичерпним списком усіх змін, внесених у .NET 8, лише ті, про які, на нашу думку, можуть бути найцікавішими. Якщо ви більше зацікавлені в покращенні продуктивності, вам слід ознайомитися з розділом «Мережі» у величезній публікації блогу про продуктивність Стівена. А якщо у вас виникнуть запитання або знайдете будь-які помилки, ви можете зв’язатися з нами в репозиторії Dotnet/Runtime .

 

Source