{"id":55435,"date":"2025-02-06T10:05:00","date_gmt":"2025-02-06T18:05:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=55435"},"modified":"2025-02-21T09:18:59","modified_gmt":"2025-02-21T17:18:59","slug":"dotnet-9-networking-improvements","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-9-networking-improvements\/","title":{"rendered":".NET 9 Networking Improvements"},"content":{"rendered":"<p>Continuing our tradition, we are excited to share a blog post highlighting the latest and most interesting changes in the networking space with the new <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-dotnet-9\/\">.NET release<\/a>. This year, we are introducing updates in the <a href=\"#http\">HTTP<\/a> space, new <a href=\"#httpclientfactory\"><code>HttpClientFactory<\/code><\/a> APIs, <a href=\"#net-framework-compatibility\">.NET Framework compatibility<\/a> improvements, and more.<\/p>\n<h2>HTTP<\/h2>\n<p>In the following section, we&#8217;re introducing the most impactful changes in the HTTP space. Among which belong perf improvements in connection pooling, support for multiple HTTP\/3 connections, auto-updating Windows proxy, and, last but not least, community contributions.<\/p>\n<h3>Connection Pooling<\/h3>\n<p>In this release, we made two impactful performance improvements in HTTP connection pooling.<\/p>\n<p>We added opt-in support for multiple HTTP\/3 connections. Using more than one HTTP\/3 connection to the peer is discouraged by the <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc9114#section-3.3\">RFC 9114<\/a> since the connection can multiplex parallel requests. However, in certain scenarios, like server-to-server, one connection might become a bottleneck even with request multiplexing. We saw such limitations with HTTP\/2 (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/35088\">dotnet\/runtime#35088<\/a>), which has the same concept of multiplexing over one connection. For the same reasons (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/51775\">dotnet\/runtime#51775<\/a>), we decided to implement multiple connection support for HTTP\/3 (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/101535\">dotnet\/runtime#101535<\/a>).<\/p>\n<p>The implementation itself tries to closely match the behavior of HTTP\/2 multiple connections. Which, at the moment, always prefer to saturate existing connections with as many requests as allowed by the peer before opening a new one. Note that this is an implementation detail and the behavior might change in the future.<\/p>\n<p>As a result, <a href=\"https:\/\/github.com\/aspnet\/Benchmarks\/tree\/main\/src\/BenchmarksApps\/HttpClientBenchmarks\">our benchmarks<\/a> showed a nontrivial increase in requests per seconds (RPS), comparison for 10,000 parallel requests:<\/p>\n<table>\n<thead>\n<tr>\n<th>client<\/th>\n<th>single HTTP\/3 connection<\/th>\n<th>multiple HTTP\/3 connections<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Max CPU Usage (%)<\/td>\n<td>35<\/td>\n<td>92<\/td>\n<\/tr>\n<tr>\n<td>Max Cores Usage (%)<\/td>\n<td>971<\/td>\n<td>2,572<\/td>\n<\/tr>\n<tr>\n<td>Max Working Set (MB)<\/td>\n<td>3,810<\/td>\n<td>6,491<\/td>\n<\/tr>\n<tr>\n<td>Max Private Memory (MB)<\/td>\n<td>4,415<\/td>\n<td>7,228<\/td>\n<\/tr>\n<tr>\n<td>Processor Count<\/td>\n<td>28<\/td>\n<td>28<\/td>\n<\/tr>\n<tr>\n<td>First request duration (ms)<\/td>\n<td>519<\/td>\n<td>594<\/td>\n<\/tr>\n<tr>\n<td>Requests<\/td>\n<td>345,446<\/td>\n<td>4,325,325<\/td>\n<\/tr>\n<tr>\n<td>Mean RPS<\/td>\n<td>23,069<\/td>\n<td>288,664<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Note that the increase in <em>Max CPU Usage<\/em> implies better CPU utilization, which means that the CPU is busy processing requests instead of being idle.<\/p>\n<p>This feature can be turned on via the <code>EnableMultipleHttp3Connections<\/code> property on <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.socketshttphandler.enablemultiplehttp3connections\"><code>SocketsHttpHandler<\/code><\/a>:<\/p>\n<pre><code class=\"language-c#\">var client = new HttpClient(new SocketsHttpHandler\n{\n    EnableMultipleHttp3Connections = true\n});<\/code><\/pre>\n<p>We also addressed lock contention in HTTP 1.1 connection pooling (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/70098\">dotnet\/runtime#70098<\/a>). The HTTP 1.1 connection pool previously used a single lock to manage the list of connections and the queue of pending requests. This lock was observed to be a bottleneck in high throughput scenarios on machines with a high number of CPU cores. We resolved this problem (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/99364\">dotnet\/runtime#99364<\/a>) by replacing an ordinary list with a lock with a concurrent collection. We chose <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.collections.concurrent.concurrentstack-1\"><code>ConcurrentStack<\/code><\/a> as it preserves the observable behavior when requests are handled by the newest available connection, which allows collecting older connections when their configured <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.socketshttphandler.pooledconnectionlifetime\">lifetime<\/a> expires. The throughput of HTTP 1.1 requests in our benchmarks increased by more than 30%:<\/p>\n<table>\n<thead>\n<tr>\n<th>Client<\/th>\n<th>.NET 8.0<\/th>\n<th>.NET 9.0<\/th>\n<th>Increase<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Requests<\/td>\n<td>80,028,791<\/td>\n<td>107,128,778<\/td>\n<td>+33.86%<\/td>\n<\/tr>\n<tr>\n<td>Mean RPS<\/td>\n<td>666,886<\/td>\n<td>892,749<\/td>\n<td>+33.87%<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>Proxy Auto Update on Windows<\/h3>\n<p>One of the main pain points when debugging HTTP traffic of applications using earlier versions of .NET is that the application doesn&#8217;t react to changes in Windows proxy settings (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/46910\">dotnet\/runtime#70098<\/a>). The proxy settings were previously initialized once per process with no reasonable ability to refresh the settings. For example (with .NET 8), <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.httpclient.defaultproxy\"><code>HttpClient.DefaultProxy<\/code><\/a> returns the same instance upon repeated access and never refetch the settings. As a result, tools like <a href=\"https:\/\/learn.microsoft.com\/microsoft-cloud\/dev\/dev-proxy\/overview\">Dev Proxy<\/a> or Fiddler, that set themselves as system proxy to listen for the traffic, weren&#8217;t able to capture traffic from already running processes. This issue was mitigated in <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/103364\">dotnet\/runtime#103364<\/a>, where the <code>HttpClient.DefaultProxy<\/code> is set to an instance of Windows proxy that listens for registry changes and reloads the proxy settings when notified.\nThe following code:<\/p>\n<pre><code class=\"language-c#\">while (true)\n{\n    using var resp = await client.GetAsync(\"https:\/\/httpbin.org\/\");\n    Console.WriteLine(HttpClient.DefaultProxy.GetProxy(new Uri(\"https:\/\/httpbin.org\/\"))?.ToString() ?? \"null\");\n    await Task.Delay(1_000);\n}<\/code><\/pre>\n<p>produces output like this:<\/p>\n<pre><code class=\"language-text\">null\n\/\/ After Fiddler's \"System Proxy\" is turned on.\nhttp:\/\/127.0.0.1:8866\/<\/code><\/pre>\n<p>Note that this change applies only for Windows as it has a unique concept of <a href=\"https:\/\/support.microsoft.com\/windows\/use-a-proxy-server-in-windows-03096c53-0554-4ffe-b6ab-8b1deee8dae1\">machine wide proxy settings<\/a>. Linux and other UNIX-based systems only allow setting up proxy via environment variables, which can&#8217;t be changed during process lifetime.<\/p>\n<h3>Community contributions<\/h3>\n<p>We&#8217;d like to call out community contributions.<\/p>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.threading.cancellationtoken\"><code>CancellationToken<\/code><\/a> overloads were missing from <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.httpcontent.loadintobufferasync\"><code>HttpContent.LoadIntoBufferAsync<\/code><\/a>. This gap was resolved by an API proposal (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/102659\">dotnet\/runtime#102659<\/a>) from <a href=\"https:\/\/github.com\/andrewhickman-aveva\">@andrewhickman-aveva<\/a> and an implementation (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/103991\">dotnet\/runtime#103991<\/a>) was from <a href=\"https:\/\/github.com\/manandre\">@manandre<\/a>.<\/p>\n<p>Another change improves a units discrepancy for the <code>MaxResponseHeadersLength<\/code> property on <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.socketshttphandler\"><code>SocketsHttpHandler<\/code><\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.httpclienthandler\"><code>HttpClientHandler<\/code><\/a> (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/75137\">dotnet\/runtime#75137<\/a>). All the other size and length properties are interpreted as being in bytes, however this one is interpreted as being in kilobytes. And since the actual behavior can&#8217;t be changed due to backward compatibility, the problem was solved by implementing an analyzer (<a href=\"https:\/\/github.com\/dotnet\/roslyn-analyzers\/pull\/6796\">dotnet\/roslyn-analyzers#6796<\/a>). The analyzer tries to make sure the user is aware that the value provided is interpreted as kilobytes, and warns if the usage suggests otherwise.<\/p>\n<p>If the value is higher than a certain threshold, it looks like this:\n<img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/02\/analyzer_warning.png\" alt=\"Analyzer Warning for MaxResponseHeadersLength\" title=\"Analyzer Warning for MaxResponseHeadersLength\" \/><\/p>\n<p>The analyzer was implemented by <a href=\"https:\/\/github.com\/amiru3f\">@amiru3f<\/a>.<\/p>\n<h2>QUIC<\/h2>\n<p>The prominent changes in QUIC space in .NET 9 include making the library public, more configuration options for connections and several performance improvements.<\/p>\n<h3>Public APIs<\/h3>\n<p>From this release on, <code>System.Net.Quic<\/code> isn&#8217;t hidden behind <a href=\"https:\/\/learn.microsoft.com\/dotnet\/fundamentals\/apicompat\/preview-apis#requirespreviewfeaturesattribute\"><code>PreviewFeature<\/code><\/a> anymore and all the APIs are generally available without any opt-in switches (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/104227\">dotnet\/runtime#104227<\/a>).<\/p>\n<h3>QUIC Connection Options<\/h3>\n<p>We expanded the configuration options for <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection\"><code>QuicConnection<\/code><\/a> (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/72984\">dotnet\/runtime#72984<\/a>). The implementation (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/94211\">dotnet\/runtime#94211<\/a>) added three new properties to <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnectionoptions\"><code>QuicConnectionOptions<\/code><\/a>:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnectionoptions.handshaketimeout\"><code>HandshakeTimeout<\/code><\/a> &#8211; we were already imposing a limit on how long a connection establishment can take, this property just enables the user to adjust it.<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnectionoptions.keepaliveinterval\"><code>KeepAliveInterval<\/code><\/a> &#8211; if this property is set up to a positive value, <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#name-ping-frames\">PING frames<\/a> are sent out regularly in this interval (in case no other activity is happening on the connection) which prevents the connection from being closed on <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#name-idle-timeout\">idle timeout<\/a>.<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnectionoptions.initialreceivewindowsizes\"><code>InitialReceiveWindowSizes<\/code><\/a> &#8211; a set of parameters to adjust the initial receive limits for data flow control sent in <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#transport-parameter-definitions\">transport parameters<\/a>. These data limits apply only until the dynamic flow control algorithm starts adjusting the limits based on the data reading speed. And due to <a href=\"https:\/\/github.com\/microsoft\/msquic\/blob\/main\/docs\/api\/QUIC_SETTINGS.md\">MsQuic<\/a> limitations, these parameters can only be set to values that are power of 2.<\/li>\n<\/ul>\n<p>All of these parameters are optional. Their default values are derived from <a href=\"https:\/\/github.com\/microsoft\/msquic\/blob\/main\/docs\/Settings.md\">MsQuic defaults<\/a>. The following code reports the defaults programmatically:<\/p>\n<pre><code class=\"language-c#\">var options = new QuicClientConnectionOptions();\nConsole.WriteLine($\"KeepAliveInterval = {PrettyPrintTimeStamp(options.KeepAliveInterval)}\");\nConsole.WriteLine($\"HandshakeTimeout = {PrettyPrintTimeStamp(options.HandshakeTimeout)}\");\nConsole.WriteLine(@$\"InitialReceiveWindowSizes =\n{{\n    Connection = {PrettyPrintInt(options.InitialReceiveWindowSizes.Connection)},\n    LocallyInitiatedBidirectionalStream = {PrettyPrintInt(options.InitialReceiveWindowSizes.LocallyInitiatedBidirectionalStream)},\n    RemotelyInitiatedBidirectionalStream = {PrettyPrintInt(options.InitialReceiveWindowSizes.RemotelyInitiatedBidirectionalStream)},\n    UnidirectionalStream = {PrettyPrintInt(options.InitialReceiveWindowSizes.UnidirectionalStream)}\n}}\");\n\nstatic string PrettyPrintTimeStamp(TimeSpan timeSpan)\n    =&gt; timeSpan == Timeout.InfiniteTimeSpan ? \"infinite\" : timeSpan.ToString();\n\nstatic string PrettyPrintInt(int sizeB)\n    =&gt; sizeB % 1024 == 0 ? $\"{sizeB \/ 1024} * 1024\" : sizeB.ToString();\n\n\/\/ Prints:\nKeepAliveInterval = infinite\nHandshakeTimeout = 00:00:10\nInitialReceiveWindowSizes =\n{\n    Connection = 16384 * 1024,\n    LocallyInitiatedBidirectionalStream = 64 * 1024,\n    RemotelyInitiatedBidirectionalStream = 64 * 1024,\n    UnidirectionalStream = 64 * 1024\n}<\/code><\/pre>\n<h3>Stream Capacity API<\/h3>\n<p>.NET 9 also introduced new APIs to support <a href=\"#connection-pooling\">multiple HTTP\/3 connections<\/a> in <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.socketshttphandler.enablemultiplehttp3connections\"><code>SocketsHttpHandler<\/code><\/a> (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/101534\">dotnet\/runtime#101534<\/a>). The APIs were designed with this specific usage in mind, and we don&#8217;t expect them to be used apart from very niche scenarios.<\/p>\n<p>QUIC has built-in logic for managing <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9000#name-max_streams-frames\">stream limits<\/a> within the protocol. As a result, calling <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection.openoutboundstreamasync\"><code>OpenOutboundStreamAsync<\/code><\/a> on a connection gets suspended if there isn&#8217;t any available stream capacity. Moreover, there isn&#8217;t an efficient way to learn whether the stream limit was reached or not. All these limitations together didn&#8217;t allow the HTTP\/3 layer to know when to open a new connection. So we introduced a new <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnectionoptions.streamcapacitycallback\"><code>StreamCapacityCallback<\/code><\/a> that gets called whenever stream capacity is increased. The callback itself is registered via <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnectionoptions\"><code>QuicConnectionOptions<\/code><\/a>. More details about the callback can be found in the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/fundamentals\/networking\/quic\/quic-options#streamcapacitycallback\">documentation<\/a>.<\/p>\n<h3>Performance Improvements<\/h3>\n<p>Both performance improvements in <code>System.Net.Quic<\/code> are TLS related and both only affect connection establishing times.<\/p>\n<p>The first performance related change was to run the peer certificate validation asynchronously in .NET thread pool (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/98361\">dotnet\/runtime#98361<\/a>). The certificate validation can be time consuming on its own and it might even include an execution of a user callback. Moving this logic to .NET thread pool stops us blocking the MsQuic thread, of which MsQuic has a limited number, and thus enables MsQuic to process higher number of new connections at the same time.<\/p>\n<p>On top of that, we have introduced caching of MsQuic configuration (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/99371\">dotnet\/runtime#99371<\/a>). MsQuic configuration is a set of native structures containing connection settings from <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnectionoptions\"><code>QuicConnectionOptions<\/code><\/a>, potentially including certificate and its intermediaries. Constructing and initializing the native structure might be very expensive since it might require serializing and deserializing all the certificate data to and from <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc7292\">PKS #12<\/a> format. Moreover, the cache  allows reusing the same MsQuic configuration for different connections if their settings are identical. Specifically server scenarios with static configuration can notably profit from the caching, like the following code:<\/p>\n<pre><code class=\"language-c#\">var alpn = \"test\";\nvar serverCertificate = X509CertificateLoader.LoadCertificateFromFile(\"..\/path\/to\/cert\");\n\n\/\/ Prepare the connection option upfront and reuse them.\nvar serverConnectionOptions = new QuicServerConnectionOptions()\n{\n    DefaultStreamErrorCode = 123,\n    DefaultCloseErrorCode = 456,\n    ServerAuthenticationOptions = new SslServerAuthenticationOptions\n    {\n        ApplicationProtocols = new List&lt;SslApplicationProtocol&gt;() { alpn },\n        \/\/ Re-using the same certificate.\n        ServerCertificate = serverCertificate\n    }\n};\n\n\/\/ Configure the listener to return the pre-prepared options.\nawait using var listener = await QuicListener.ListenAsync(new QuicListenerOptions()\n{\n    ListenEndPoint = new IPEndPoint(IPAddress.Loopback, 0),\n    ApplicationProtocols = [ alpn ],\n    \/\/ Callback returns the same object.\n    \/\/ Internal cache will re-use the same native structure for every incoming connection.\n    ConnectionOptionsCallback = (_, _, _) =&gt; ValueTask.FromResult(serverConnectionOptions)\n});<\/code><\/pre>\n<p>We also built it an escape hatch for this feature, it can be turned off with either environment variable:<\/p>\n<pre><code class=\"language-sh\">export DOTNET_SYSTEM_NET_QUIC_DISABLE_CONFIGURATION_CACHE=1\n# run the app<\/code><\/pre>\n<p>or with an <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.appcontext\">AppContext<\/a> switch:<\/p>\n<pre><code class=\"language-c#\">AppContext.SetSwitch(\"System.Net.Quic.DisableConfigurationCache\", true);<\/code><\/pre>\n<h2>WebSockets<\/h2>\n<p>.NET 9 introduces the long-desired PING\/PONG Keep-Alive strategy to WebSockets (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/48729\">dotnet\/runtime#48729<\/a>).<\/p>\n<p>Prior to .NET 9, the only available Keep-Alive strategy was Unsolicited PONG. It was enough to keep the underlying TCP connection from idling out, but in a case when a remote host becomes unresponsive (for example, a remote server crashes), the only way to detect such situations was to depend on the TCP timeout.<\/p>\n<p>In this release, we complement the existing <code>KeepAliveInterval<\/code> setting with the new <code>KeepAliveTimeout<\/code> setting, so that the Keep-Alive strategy is selected as follows:<\/p>\n<ol>\n<li>Keep-Alive is <strong>OFF<\/strong>, if\n<ul>\n<li><code>KeepAliveInterval<\/code> is <code>TimeSpan.Zero<\/code> or <code>Timeout.InfiniteTimeSpan<\/code><\/li>\n<\/ul>\n<\/li>\n<li><strong>Unsolicited PONG<\/strong>, if\n<ul>\n<li><code>KeepAliveInterval<\/code> is a positive finite <code>TimeSpan<\/code>, -AND-<\/li>\n<li><code>KeepAliveTimeout<\/code> is <code>TimeSpan.Zero<\/code> or <code>Timeout.InfiniteTimeSpan<\/code><\/li>\n<\/ul>\n<\/li>\n<li><strong>PING\/PONG<\/strong>, if\n<ul>\n<li><code>KeepAliveInterval<\/code> is a positive finite <code>TimeSpan<\/code>, -AND-<\/li>\n<li><code>KeepAliveTimeout<\/code> is a positive finite <code>TimeSpan<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ol>\n<p>By default, the preexisting Keep-Alive behavior is maintained: <code>KeepAliveTimeout<\/code> default value is <code>Timeout.InfiniteTimeSpan<\/code>, so Unsolicited PONG remains as the default strategy.<\/p>\n<p>The following example illustrates how to enable the PING\/PONG strategy for a <code>ClientWebSocket<\/code>:<\/p>\n<pre><code class=\"language-csharp\">var cws = new ClientWebSocket();\ncws.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);\ncws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);\nawait cws.ConnectAsync(uri, cts.Token);\n\n\/\/ NOTE: There should be an outstanding read at all times to\n\/\/ ensure incoming PONGs are promptly processed\nvar result = await cws.ReceiveAsync(buffer, cts.Token);<\/code><\/pre>\n<p>If no PONG response received after <code>KeepAliveTimeout<\/code> elapsed, the remote endpoint is deemed unresponsive, and the WebSocket connection is automatically aborted. It also unblocks the outstanding <code>ReceiveAsync<\/code> with an <code>OperationCanceledException<\/code>.<\/p>\n<p>To learn more about the feature, you can check out the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/fundamentals\/networking\/websockets#keep-alive-strategies\">dedicated conceptual docs<\/a>.<\/p>\n<h2>.NET Framework Compatibility<\/h2>\n<p>One of the biggest hurdles in the networking space when migrating projects from .NET Framework to .NET Core is the difference between the HTTP stacks. In .NET Framework, the main class to handle HTTP requests is <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.httpwebrequest\"><code>HttpWebRequest<\/code><\/a> which uses global <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.servicepointmanager\"><code>ServicePointManager<\/code><\/a> and individual <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.servicepoint\"><code>ServicePoint<\/code>s<\/a> to handle connection pooling. Whereas in .NET Core, <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.httpclient\"><code>HttpClient<\/code><\/a> is the recommended way to access HTTP resources. On top of that, all the classes from .NET Framework are present in .NET, but they&#8217;re either obsolete, or missing implementation, or are just not maintained at all. As a result, we often see mistakes like using <code>ServicePointManager<\/code> to configure the connections while using <code>HttpClient<\/code> to access the resources.<\/p>\n<p>The recommendation always was to fully migrate to <code>HttpClient<\/code>, but sometimes it&#8217;s not possible. Migrating projects from .NET Framework to .NET Core can be difficult on its own, let alone rewriting all the networking code. Expecting customers to do all this work in one step proved to be unrealistic and is one of the reasons why customers might be reluctant to migrate. To mitigate these pain points, we filled in some missing implementations of the legacy classes and created a comprehensive guide to help with the migration.<\/p>\n<p>The first part is expansion of supported <code>ServicePointManager<\/code> and <code>ServicePoint<\/code> properties that were missing implementation in .NET Core up until this release (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/94664\">dotnet\/runtime#94664<\/a> and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/97537\">dotnet\/runtime#97537<\/a>). With these changes, they&#8217;re now taken into account when using <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.httpwebrequest\"><code>HttpWebRequest<\/code><\/a>.<\/p>\n<p>For <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.httpwebrequest\"><code>HttpWebRequest<\/code><\/a>, we implemented full support of <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.httpwebrequest.allowwritestreambuffering\"><code>AllowWriteStreamBuffering<\/code><\/a> in <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/95001\">dotnet\/runtime#95001<\/a>. And also added missing support for <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.webrequest.impersonationlevel\"><code>ImpersonationLevel<\/code><\/a> in <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/102038\">dotnet\/runtime#102038<\/a>.<\/p>\n<p>On top of these changes, we also obsoleted a few legacy classes to prevent further confusion:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.servicepointmanager\"><code>ServicePointManager<\/code><\/a> in <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/103456\">dotnet\/runtime#103456<\/a>. Its settings have no effect on <code>HttpClient<\/code> and <code>SslStream<\/code> while it might be misused in good faith for exactly that purpose.<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.authenticationmanager\"><code>AuthenticationManager<\/code><\/a> in <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/93171\">dotnet\/runtime#93171<\/a>, done by community contributor <a href=\"https:\/\/github.com\/deeprobin\">@deeprobin<\/a>. It&#8217;s either missing implementation or the methods throw <code>PlatformNotSupportedException<\/code>.<\/li>\n<\/ul>\n<p>Lastly, we put up together a guide for migration from <code>HttpWebRequest<\/code> to <code>HttpClient<\/code> in <a href=\"https:\/\/learn.microsoft.com\/dotnet\/fundamentals\/networking\/http\/httpclient-migrate-from-httpwebrequest\"><em>HttpWebRequest to HttpClient migration guide<\/em><\/a>. It includes comprehensive lists of mappings between individual properties and methods, e.g., <a href=\"https:\/\/learn.microsoft.com\/dotnet\/fundamentals\/networking\/http\/httpclient-migrate-from-httpwebrequest#migrate-servicepointmanager-usage\"><em>Migrate ServicePoint(Manager) usage<\/em><\/a> and many examples for trivial and not so trivial scenarios, e.g., <a href=\"https:\/\/learn.microsoft.com\/dotnet\/fundamentals\/networking\/http\/httpclient-migrate-from-httpwebrequest#example-enable-dns-round-robin\"><em>Example: Enable DNS round robin<\/em><\/a>.<\/p>\n<h2>Diagnostics<\/h2>\n<p>In this release, diagnostics improvements focus on enhancing privacy protection and advancing distributed tracing capabilities.<\/p>\n<h3>Uri Query Redaction in HttpClientFactory Logs<\/h3>\n<p>Starting with version <code>9.0.0<\/code> of <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Extensions.Http\"><code>Microsoft.Extensions.Http<\/code><\/a>, the default logging logic of <code>HttpClientFactory<\/code> prioritizes <em>protecting privacy<\/em>. In older versions, it emits the full request URI in the <code>RequestStart<\/code> and <code>RequestPipelineStart<\/code> events. In cases where some components of the URI contain sensitive information, this can lead to privacy incidents by leaking such data into logs.<\/p>\n<p>Version <code>8.0.0<\/code> introduced the ability to secure <code>HttpClientFactory<\/code> usage by <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-8-networking-improvements\/#modify-httpclient-logging\">customizing logging<\/a>. However, this doesn&#8217;t change the fact that the default behavior might be risky for unaware users.<\/p>\n<p>In the majority of the problematic cases, sensitive information resides in the query component. Therefore, a <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/compatibility\/networking\/9.0\/query-redaction-logs\">breaking change<\/a> was introduced in <code>9.0.0<\/code>, removing the entire query string from <code>HttpClientFactory<\/code> logs by default. A global <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/compatibility\/networking\/9.0\/query-redaction-logs#recommended-action\">opt-out switch<\/a> is available for services\/apps where it&#8217;s safe to log the full URI.<\/p>\n<p>For consistency and maximum safety, a <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/compatibility\/networking\/9.0\/query-redaction-events\">similar change<\/a> was implemented for <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.diagnostics.tracing.eventsource\"><code>EventSource<\/code><\/a> events in <code>System.Net.Http<\/code>.<\/p>\n<p>We recognize that this solution might not suit everyone. Ideally, there should be a fine-grained URI filtering mechanism, allowing users to retain non-sensitive query entries or filter other URI components (e.g., parts of the path). We plan to explore such a feature for future versions (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/110018\">dotnet\/runtime#110018<\/a>).<\/p>\n<h3>Distributed Tracing Improvements<\/h3>\n<p><a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/diagnostics\/distributed-tracing\">Distributed tracing<\/a> is a diagnostic technique for tracking the path of a specific transaction across multiple processes and machines, helping identify bottlenecks and failures. This technique models the transaction as a hierarchical tree of <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/diagnostics\/distributed-tracing-concepts#traces-and-activities\"><code>Activities<\/code><\/a>, also referred to as <a href=\"https:\/\/opentelemetry.io\/docs\/concepts\/signals\/traces\/#spans\">spans<\/a> in OpenTelemetry terminology.<\/p>\n<p><code>HttpClientHandler<\/code> and <code>SocketsHttpHandler<\/code> are instrumented to start an <code>Activity<\/code> for each request and propagate the <a href=\"https:\/\/www.w3.org\/TR\/trace-context\/\">trace context<\/a> via standard W3C headers when tracing is enabled.<\/p>\n<p>Before .NET 9, users needed the OpenTelemetry .NET SDK to produce useful OpenTelemetry-compliant traces. This SDK was required not just for collection and export but also to <a href=\"https:\/\/github.com\/open-telemetry\/opentelemetry-dotnet-contrib\/blob\/85afc5dd1bd7deef9b8949f36a14cd3fd1aacecb\/src\/OpenTelemetry.Instrumentation.Http\/README.md\">extend the instrumentation<\/a>, as the built-in logic didn&#8217;t populate the <code>Activity<\/code> with request data.<\/p>\n<p>Starting with .NET 9, the instrumentation dependency (<code>OpenTelemetry.Instrumentation.Http<\/code>) can be omitted unless advanced features like enrichment are required. In <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/104251\">dotnet\/runtime#104251<\/a>, we extended the built-in tracing to ensure that the shape of the <code>Activity<\/code> is <a href=\"https:\/\/opentelemetry.io\/docs\/specs\/semconv\/http\/http-spans\/#http-client\">OTel-compliant<\/a>, with the name, status, and most required tags populated according to the standard.<\/p>\n<h4>Experimental Connection Tracing<\/h4>\n<p>When investigating bottlenecks, you might want to zoom into specific HTTP requests to identify where most of the time is spent. Is it during a connection establishment or the content download? If there are connection issues, it\u2019s helpful to determine whether the problem lies with DNS lookups, TCP connection establishment, or the TLS handshake.<\/p>\n<p>.NET 9 has introduced several new spans to represent activities around connection establishment in <code>SocketsHttpHandler<\/code>. The most significant one <code>HTTP connection setup<\/code> span which breaks down to three child spans for DNS, TCP, and TLS activities.<\/p>\n<p>Because connection setup isn&#8217;t tied to a particular request in <code>SocketsHttpHandler<\/code> connection pool, the connection setup span can\u2019t be modeled as a child span of the HTTP client request span. Instead, the relationship between requests and connections is being represented using <a href=\"https:\/\/opentelemetry.io\/docs\/concepts\/signals\/traces\/#span-links\">Span Links<\/a>, also known as Activity Links.<\/p>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Note<\/strong><\/p>The new spans are produced by various ActivitySources matching the wildcard <code>Experimental.System.Net.*<\/code>. These spans are experimental because monitoring tools like <a href=\"https:\/\/learn.microsoft.com\/azure\/azure-monitor\/app\/app-insights-overview\">Azure Monitor Application Insights<\/a> have difficulty visualizing the resulting traces effectively due to the numerous <code>connection_setup \u2192 request<\/code> backlinks. To improve the user experience in monitoring tools, further work is needed. It involves collaboration between the .NET team, OTel, and tool authors, and may result in breaking changes in the design of the new spans.<\/div><\/p>\n<p>The simplest way to set up and try connection trace collection is by using <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/get-started\/aspire-overview\">.NET Aspire<\/a>. Using <a href=\"https:\/\/learn.microsoft.com\/dotnet\/aspire\/fundamentals\/dashboard\/overview\">Aspire Dashboards<\/a> it&#8217;s possible to expand the <code>connection_setup<\/code> activity and see a breakdown of the connection initialization.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/devblogs.microsoft.com\/dotnet\/wp-content\/uploads\/sites\/10\/2025\/02\/connection-setup.png\" alt=\"A breakdown of the connection_setup activity on .NET Aspire Dashboards\" \/><\/p>\n<p>If you think the .NET 9 tracing additions might bring you valuable diagnostic insights, and you want to get some hands-on experience, don&#8217;t hesitate to read our full article about <a href=\"https:\/\/learn.microsoft.com\/dotnet\/fundamentals\/networking\/telemetry\/tracing\">Distributed tracing in System.Net libraries<\/a>.<\/p>\n<h2>HttpClientFactory<\/h2>\n<p>For <code>HttpClientFactory<\/code>, we&#8217;re introducing the Keyed DI support, offering a new convenient consumption pattern, and changing a default Primary Handler to mitigate a common erroneous usecase.<\/p>\n<h3>Keyed DI Support<\/h3>\n<p>In the previous release, <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/extensions\/dependency-injection?source=recommendations#keyed-services\">Keyed Services<\/a> were introduced to <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Extensions.DependencyInjection\"><code>Microsoft.Extensions.DependencyInjection<\/code><\/a> packages. Keyed DI allows you to specify the keys while registering multiple implementations of a single service type&mdash;and to later retrieve a specific implementation using the respective key.<\/p>\n<p><code>HttpClientFactory<\/code> and named <code>HttpClient<\/code> instances, unsurprisingly, align well with the Keyed Services idea. Among other things, <code>HttpClientFactory<\/code> was a way to overcome this long-missing DI feature. But it required you to obtain, store and query the <code>IHttpClientFactory<\/code> instance&mdash;instead of simply injecting a configured <code>HttpClient<\/code>&mdash;which might be inconvenient. While Typed clients attempted to simplify that part, it came with a catch: Typed clients are easy to <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/extensions\/httpclient-factory-troubleshooting#typed-client-has-the-wrong-httpclient-injected\">misconfigure<\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/extensions\/httpclient-factory#avoid-typed-clients-in-singleton-services\">misuse<\/a> (and the supporting infra can also be a tangible overhead in <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/66863\">certain scenarios<\/a>). As a result, the user experience in both cases was far from ideal.<\/p>\n<p>This changes as <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Extensions.DependencyInjection\"><code>Microsoft.Extensions.DependencyInjection<\/code><\/a> <code>9.0.0<\/code> and <a href=\"https:\/\/www.nuget.org\/packages\/Microsoft.Extensions.Http\"><code>Microsoft.Extensions.Http<\/code><\/a> <code>9.0.0<\/code> packages bring the Keyed DI support into <code>HttpClientFactory<\/code> (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/89755\">dotnet\/runtime#89755<\/a>). Now you can have the best of both worlds: you can pair the convenient, highly configurable <code>HttpClient<\/code> registrations with the straightforward injection of the specific configured <code>HttpClient<\/code> instances.<\/p>\n<p>As of <code>9.0.0<\/code>, you need to <em>opt in<\/em> to the feature by calling the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.addaskeyed\"><code>AddAsKeyed()<\/code><\/a> extension method. It registers a Named <code>HttpClient<\/code> as a Keyed service for the key equal to the client&#8217;s name&mdash;and enables you to use the Keyed Services APIs (e.g., <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute\"><code>[FromKeyedServices(...)]<\/code><\/a>) to obtain the required <code>HttpClient<\/code>s.<\/p>\n<p>The following code demonstrates the integration between <code>HttpClientFactory<\/code>, Keyed DI and ASP.NET Core 9.0 Minimal APIs:<\/p>\n<pre><code class=\"language-csharp\">var builder = WebApplication.CreateBuilder(args);\n\nbuilder.Services.AddHttpClient(\"github\", c =&gt;\n    {\n        c.BaseAddress = new Uri(\"https:\/\/api.github.com\/\");\n        c.DefaultRequestHeaders.Add(\"Accept\", \"application\/vnd.github.v3+json\");\n        c.DefaultRequestHeaders.Add(\"User-Agent\", \"dotnet\");\n    })\n    .AddAsKeyed(); \/\/ Add HttpClient as a Keyed Scoped service for key=\"github\"\n\nvar app = builder.Build();\n\n\/\/ Directly inject the Keyed HttpClient by its name\napp.MapGet(\"\/\", ([FromKeyedServices(\"github\")] HttpClient httpClient) =&gt;\n    httpClient.GetFromJsonAsync&lt;Repo&gt;(\"\/repos\/dotnet\/runtime\"));\n\napp.Run();\n\nrecord Repo(string Name, string Url);<\/code><\/pre>\n<p>Endpoint response:<\/p>\n<pre><code class=\"language-sh\">&gt; ~  curl http:\/\/localhost:5000\/\n{\"name\":\"runtime\",\"url\":\"https:\/\/api.github.com\/repos\/dotnet\/runtime\"}<\/code><\/pre>\n<p>By default, <code>AddAsKeyed()<\/code> registers <code>HttpClient<\/code> as a Keyed <em>Scoped<\/em> service. The Scoped lifetime can help catching cases of captive dependencies:<\/p>\n<pre><code class=\"language-csharp\">services.AddHttpClient(\"scoped\").AddAsKeyed();\nservices.AddSingleton&lt;CapturingSingleton&gt;();\n\n\/\/ Throws: Cannot resolve scoped service 'System.Net.Http.HttpClient' from root provider.\nrootProvider.GetRequiredKeyedService&lt;HttpClient&gt;(\"scoped\");\n\nusing var scope = provider.CreateScope();\nscope.ServiceProvider.GetRequiredKeyedService&lt;HttpClient&gt;(\"scoped\"); \/\/ OK\n\n\/\/ Throws: Cannot consume scoped service 'System.Net.Http.HttpClient' from singleton 'CapturingSingleton'.\npublic class CapturingSingleton([FromKeyedServices(\"scoped\")] HttpClient httpClient)\n\/\/{ ...<\/code><\/pre>\n<p>You can also explicitly specify the lifetime by passing the <code>ServiceLifetime<\/code> parameter to the <code>AddAsKeyed()<\/code> method:<\/p>\n<pre><code class=\"language-csharp\">services.AddHttpClient(\"explicit-scoped\")\n    .AddAsKeyed(ServiceLifetime.Scoped);\n\nservices.AddHttpClient(\"singleton\")\n    .AddAsKeyed(ServiceLifetime.Singleton);<\/code><\/pre>\n<p>You don&#8217;t have to call <code>AddAsKeyed<\/code> for every single client&mdash;you can easily opt in &#8220;globally&#8221; (for any client name) via <code>ConfigureHttpClientDefaults<\/code>. From Keyed Services perspective, it results in the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.extensions.dependencyinjection.keyedservice.anykey\"><code>KeyedService.AnyKey<\/code><\/a> registration.<\/p>\n<pre><code class=\"language-csharp\">services.ConfigureHttpClientDefaults(b =&gt; b.AddAsKeyed());\n\nservices.AddHttpClient(\"foo\", \/* ... *\/);\nservices.AddHttpClient(\"bar\", \/* ... *\/);\n\npublic class MyController(\n    [FromKeyedServices(\"foo\")] HttpClient foo,\n    [FromKeyedServices(\"bar\")] HttpClient bar)\n\/\/{ ...<\/code><\/pre>\n<p>Even though the &#8220;global&#8221; opt-in is a one-liner, it&#8217;s unfortunate that the feature still requires it, instead of just working &#8220;out of the box&#8221;. For full context and reasoning on that decision, see <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/89755\">dotnet\/runtime#89755<\/a> and <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/104943\">dotnet\/runtime#104943<\/a>.<\/p>\n<p>You can explicitly opt out from Keyed DI for <code>HttpClient<\/code>s by calling <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.removeaskeyed\"><code>RemoveAsKeyed()<\/code><\/a> (for example, per specific client, in case of the &#8220;global&#8221; opt-in):<\/p>\n<pre><code class=\"language-csharp\">services.ConfigureHttpClientDefaults(b =&gt; b.AddAsKeyed());      \/\/ opt IN by default\nservices.AddHttpClient(\"keyed\", \/* ... *\/);\nservices.AddHttpClient(\"not-keyed\", \/* ... *\/).RemoveAsKeyed(); \/\/ opt OUT per name\n\nprovider.GetRequiredKeyedService&lt;HttpClient&gt;(\"keyed\");     \/\/ OK\nprovider.GetRequiredKeyedService&lt;HttpClient&gt;(\"not-keyed\"); \/\/ Throws: No service for type 'System.Net.Http.HttpClient' has been registered.\nprovider.GetRequiredKeyedService&lt;HttpClient&gt;(\"unknown\");   \/\/ OK (unconfigured instance)<\/code><\/pre>\n<p>If called together, or any of them more than once, <code>AddAsKeyed()<\/code> and <code>RemoveAsKeyed()<\/code> generally follow the rules of <code>HttpClientFactory<\/code> configs and DI registrations:<\/p>\n<ol>\n<li>If used within the same name, the last setting wins: the lifetime from the last <code>AddAsKeyed()<\/code> is used to create the Keyed registration (unless <code>RemoveAsKeyed()<\/code> was called last, in which case the name is excluded).<\/li>\n<li>If used only within <code>ConfigureHttpClientDefaults<\/code>, the last setting wins.<\/li>\n<li>If both <code>ConfigureHttpClientDefaults<\/code> and a specific client name were used, all defaults are considered to &#8220;happen&#8221; before all per-name settings for this client. Thus, the defaults can be disregarded, and the last of the per-name ones wins.<\/li>\n<\/ol>\n<p>You can learn more about the feature in the <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/extensions\/httpclient-factory-keyed-di\">dedicated conceptual docs<\/a>.<\/p>\n<h3>Default Primary Handler Change<\/h3>\n<p>One of the most common problems <code>HttpClientFactory<\/code> users run into is when a Named or a Typed client erroneously gets <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/extensions\/httpclient-factory#avoid-typed-clients-in-singleton-services\">captured in a Singleton service<\/a>, or, in general, stored somewhere for a period of time that&#8217;s longer than the specified <code>HandlerLifetime<\/code>. Because <code>HttpClientFactory<\/code> can&#8217;t rotate such handlers, they might end up <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/extensions\/httpclient-factory-troubleshooting#httpclient-doesnt-respect-dns-changes\">not respecting DNS changes<\/a>. It is, unfortunately, easy and seemingly &#8220;intuitive&#8221; to inject a Typed client into a singleton, but hard to have any kind of check\/analyzer to make sure <code>HttpClient<\/code> isn&#8217;t captured when it wasn&#8217;t supposed to. It might be even harder to troubleshoot the resulting issues.<\/p>\n<p>On the other hand, the problem can be <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/extensions\/httpclient-factory#using-ihttpclientfactory-together-with-socketshttphandler\">mitigated<\/a> by using <code>SocketsHttpHandler<\/code>, which can control <code>PooledConnectionLifetime<\/code>. Similarly to <code>HandlerLifetime<\/code>, it allows regularly recreating connections to pick up the DNS changes, but on a lower level. A client with <code>PooledConnectionLifetime<\/code> set up can be safely used as a Singleton.<\/p>\n<p>Therefore, to minimize the potential impact of the erroneous usage patterns, .NET 9 makes the default Primary handler a <code>SocketsHttpHandler<\/code> (on platforms that support it; other platforms, e.g. .NET Framework, continue to use <code>HttpClientHandler<\/code>). And most importantly, <code>SocketsHttpHandler<\/code> also has the <code>PooledConnectionLifetime<\/code> property <em>preset<\/em> to match the <code>HandlerLifetime<\/code> value (it reflects the latest value, if you configured <code>HandlerLifetime<\/code> one or more times).<\/p>\n<p>The change only affects cases when the client was <em>not<\/em> configured to have a custom Primary handler (via e.g. <code>ConfigurePrimaryHttpMessageHandler&lt;T&gt;()<\/code>).<\/p>\n<p>While the default Primary handler is an implementation detail, as it was never specified in the docs, it&#8217;s still considered a <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/compatibility\/networking\/9.0\/default-handler\">breaking change<\/a>. There could be cases in which you wanted to use the specific type, for example, casting the Primary handler to <code>HttpClientHandler<\/code> to set properties like <code>ClientCertificates<\/code>, <code>UseCookies<\/code>, <code>UseProxy<\/code>, etc. If you need to use such properties, it&#8217;s suggested to check for both <code>HttpClientHandler<\/code> and <code>SocketsHttpHandler<\/code> in the configuration action:<\/p>\n<pre><code class=\"language-csharp\">services.AddHttpClient(\"test\")\n    .ConfigurePrimaryHttpMessageHandler((h, _) =&gt;\n    {\n        if (h is HttpClientHandler hch)\n        {\n            hch.UseCookies = false;\n        }\n\n        if (h is SocketsHttpHandler shh)\n        {\n            shh.UseCookies = false;\n        }\n    });<\/code><\/pre>\n<p>Alternatively, you can explicitly specify a Primary handler for each of your clients:<\/p>\n<pre><code class=\"language-csharp\">services.AddHttpClient(\"test\")\n    .ConfigurePrimaryHttpMessageHandler(() =&gt; new HttpClientHandler() { UseCookies = false });<\/code><\/pre>\n<p>Or, configure the default Primary handler for all clients using <code>ConfigureHttpClientDefaults<\/code>:<\/p>\n<pre><code class=\"language-csharp\">services.ConfigureHttpClientDefaults(b =&gt;\n    b.ConfigurePrimaryHttpMessageHandler(() =&gt; new HttpClientHandler() { UseCookies = false }));<\/code><\/pre>\n<h2>Security<\/h2>\n<p>In <code>System.Net.Security<\/code>, we&#8217;re introducing the highly sought support for SSLKEYLOGFILE, more scenarios supporting TLS resume, and new additions in negotiate APIs.<\/p>\n<h3>SSLKEYLOGFILE Support<\/h3>\n<p>The most upvoted issue in the security space was to support logging of pre-master secret (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/37915\">dotnet\/runtime#37915<\/a>). The logged secret can be used by packet capturing tool Wireshark to decrypt the traffic. It&#8217;s a useful diagnostics tool when investigating networking issues. Moreover, the same functionality is provided by browsers like Firefox (via <a href=\"https:\/\/udn.realityripple.com\/docs\/Mozilla\/Projects\/NSS\/Key_Log_Format\">NSS<\/a>) and Chrome and command line HTTP tools like <a href=\"https:\/\/everything.curl.dev\/usingcurl\/tls\/sslkeylogfile\">cURL<\/a>.\nWe have implemented this feature for both <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslstream\"><code>SslStream<\/code><\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.quic.quicconnection\"><code>QuicConnection<\/code><\/a>. For the former, the functionality is limited to the platforms on which we use OpenSSL as a cryptographic library. In the terms of the officially released .NET runtime, it means only on Linux operating systems. For the latter, it&#8217;s supported everywhere, regardless of the cryptographic library. That&#8217;s because TLS is part of the QUIC protocol (<a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9001.html\">RFC 9001<\/a>) so the user-space MsQuic has access to all the secrets and so does .NET. The limitation of <code>SslStream<\/code> on Windows comes from SChannel using a separate, privileged process for TLS which won&#8217;t allow exporting secrets due to security concerns (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/94843\">dotnet\/runtime#94843<\/a>).<\/p>\n<p>This feature exposes security secrets and relying solely on an environmental variable could unintentionally leak them. For that reason, we&#8217;ve decided to introduce an additional <code>AppContext<\/code> switch necessary to enable the feature (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/100665\">dotnet\/runtime#100665<\/a>). It requires the user to prove the ownership of the application by either setting it programmatically in the code:<\/p>\n<pre><code class=\"language-c#\">AppContext.SetSwitch(\"System.Net.EnableSslKeyLogging\", true);<\/code><\/pre>\n<p>or by changing the <code>{appname}.runtimeconfig.json<\/code> next to the application:<\/p>\n<pre><code class=\"language-c#\">{\n  \"runtimeOptions\": {\n    \"configProperties\": {\n      \"System.Net.EnableSslKeyLogging\": true\n    }\n  }\n}<\/code><\/pre>\n<p>The last thing is to set up an environmental variable <code>SSLKEYLOGFILE<\/code> and run the application:<\/p>\n<pre><code class=\"language-sh\">export SSLKEYLOGFILE=~\/keylogfile\n\n.\/&lt;appname&gt;<\/code><\/pre>\n<p>At this point, <code>~\/keylogfile<\/code> will contain pre-master secrets that can be used by Wireshark to decrypt the traffic. For more information, see <a href=\"https:\/\/wiki.wireshark.org\/TLS#using-the-pre-master-secret\">TLS Using the (Pre)-Master-Secret<\/a> documentation.<\/p>\n<h3>TLS Resume with Client Certificate<\/h3>\n<p>TLS resume enables reusing previously stored TLS data to re-establish connection to previously connected server. It can save round trips during the handshake as well as CPU processing. This feature is a native part of Windows SChannel, therefore it&#8217;s implicitly used by .NET on Windows platforms. However, on Linux platforms where we use OpenSSL as a cryptographic library, enabling caching and reusing TLS data is more involved. We first introduced the support in .NET 7 (see <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-7-networking-improvements\/#performance\">TLS Resume<\/a>). It has its own limitations that in general are not present on Windows. One such limitation was that it was not supported for sessions using mutual authentication by providing a client certificate (<a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/94561\">dotnet\/runtime#94561<\/a>). It has been fixed in .NET 9 (<a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/102656\">dotnet\/runtime#102656<\/a>) and works if one these properties is set as described:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslclientauthenticationoptions.clientcertificatecontext\"><code>ClientCertificateContext<\/code><\/a><\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslclientauthenticationoptions.localcertificateselectioncallback\"><code>LocalCertificateSelectionCallback<\/code><\/a> returns non-null certificate on the first call<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.sslclientauthenticationoptions.clientcertificates\"><code>ClientCertificates<\/code><\/a> collection has at least one certificate with private key<\/li>\n<\/ul>\n<h3>Negotiate API Integrity Checks<\/h3>\n<p>In .NET 7, we added <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.security.negotiateauthentication\"><code>NegotiateAuthentication<\/code><\/a> APIs, see <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/dotnet-7-networking-improvements\/#negotiate-api\">Negotiate API<\/a>. The original implementation&#8217;s goal was to remove access via reflection to the internals of <code>NTAuthentication<\/code>. However, that proposal was missing functions to generate and verify message integrity codes from <a href=\"https:\/\/datatracker.ietf.org\/doc\/html\/rfc2743\">RFC 2743<\/a>. They&#8217;re usually implemented as cryptographic signing operation with a negotiated key. The API was proposed in <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/86950\">dotnet\/runtime#86950<\/a> and implemented in <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/96712\">dotnet\/runtime#96712<\/a> and as the original change, all the work from the API proposal to the implementation was done by a community contributor <a href=\"https:\/\/github.com\/filipnavara\">@filipnavara<\/a>.<\/p>\n<h2>Networking Primitives<\/h2>\n<p>This section encompasses changes in <code>System.Net<\/code> namespace. We&#8217;re introducing new support for server-side events and some small additions in APIs, for example new MIME types.<\/p>\n<h3>Server-Sent Events Parser<\/h3>\n<p>Server-sent events is a technology that allows servers to push data updates on clients via an HTTP connection. It is defined in <a href=\"https:\/\/html.spec.whatwg.org\/multipage\/server-sent-events.html\">living HTML standard<\/a>. It uses <code>text\/event-stream<\/code> MIME type and it&#8217;s always decoded as <code>UTF-8<\/code>. The advantage of the server-push approach over client-pull is that it can make better use of network resources and also save battery life of mobile devices.<\/p>\n<p>In this release, we&#8217;re introducing an OOB package <code>System.Net.ServerSentEvents<\/code>. It&#8217;s available as a .NET Standard 2.0 <a href=\"https:\/\/www.nuget.org\/packages\/System.Net.ServerSentEvents\">NuGet package<\/a>. The package offers a parser for server-sent event stream, following the <a href=\"https:\/\/html.spec.whatwg.org\/multipage\/server-sent-events.html#parsing-an-event-stream\">specification<\/a>. The protocol is stream based, with individual items separated by an empty line.<\/p>\n<p>Each item has two fields:<\/p>\n<ul>\n<li><code>type<\/code> &#8211; default type is <code>message<\/code><\/li>\n<li><code>data<\/code> &#8211; data itself<\/li>\n<\/ul>\n<p>On top of that, there are two other optional fields that progressively update properties of the stream:<\/p>\n<ul>\n<li><code>id<\/code> &#8211; determines the last event id that is sent in <a href=\"https:\/\/html.spec.whatwg.org\/multipage\/server-sent-events.html#the-last-event-id-header\"><code>Last-Event-Id<\/code> header<\/a> in case the connection needs to be reconnected<\/li>\n<li><code>retry<\/code> &#8211; number of milliseconds to wait between reconnection attempts<\/li>\n<\/ul>\n<p>The library APIs were proposed in <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/98105\">dotnet\/runtime#98105<\/a> and contain type definitions for the parser and the items:<\/p>\n<ul>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.serversentevents.sseparser\"><code>SseParser<\/code><\/a> &#8211; static class to create the actual parser from the stream, allowing the user to optionally provide a parsing delegate for the item data<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.serversentevents.sseparser-1\"><code>SseParser&lt;T&gt;<\/code><\/a> &#8211; parser itself, offers methods to enumerate (synchronously or asynchronously) the stream and return the parsed items<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.serversentevents.sseitem-1\"><code>SseItem&lt;T&gt;<\/code><\/a> &#8211; struct holding parsed item data<\/li>\n<\/ul>\n<p>Then the parser can be used like this, for example:<\/p>\n<pre><code class=\"language-c#\">using HttpClient client = new HttpClient();\nusing Stream stream = await client.GetStreamAsync(\"https:\/\/server\/sse\");\n\nvar parser = SseParser.Create(stream, (type, data) =&gt;\n{\n    var str = Encoding.UTF8.GetString(data);\n    return Int32.Parse(str);\n});\nawait foreach (var item in parser.EnumerateAsync())\n{\n    Console.WriteLine($\"{item.EventType}: {item.Data} [{parser.LastEventId};{parser.ReconnectionInterval}]\");\n}<\/code><\/pre>\n<p>And for the following input:<\/p>\n<pre><code class=\"language-text\">: stream of integers\n\ndata: 123\nid: 1\nretry: 1000\n\ndata: 456\nid: 2\n\ndata: 789\nid: 3\n<\/code><\/pre>\n<p>It outputs:<\/p>\n<pre><code class=\"language-text\">message: 123 [1;00:00:01]\nmessage: 456 [2;00:00:01]\nmessage: 789 [3;00:00:01]<\/code><\/pre>\n<h3>Primitives Additions<\/h3>\n<p>Apart from server sent event, <code>System.Net<\/code> namespace got a few small other additions:<\/p>\n<ul>\n<li>\n<p><code>IEquatable&lt;Uri&gt;<\/code> interface implementation for <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.uri.equals#system-uri-equals%28system-uri%29\"><code>Uri<\/code><\/a> in <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/97940\">dotnet\/runtime#97940<\/a><\/p>\n<p>Which allows using <code>Uri<\/code> in functions that require <code>IEquatable<\/code> like <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.memoryextensions.contains#system-memoryextensions-contains-1%28system-readonlyspan%28%28-0%29%29-0%29\"><code>Span.Contains<\/code><\/a> or <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.memoryextensions.sequenceequal#system-memoryextensions-sequenceequal-1%28system-readonlyspan%28%28-0%29%29-system-readonlyspan%28%28-0%29%29%29\"><code>SequenceEquals<\/code><\/a>.<\/p>\n<\/li>\n<li>\n<p>span-based <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.uri.escapedatastring#system-uri-escapedatastring%28system-readonlyspan%28%28system-char%29%29%29\"><code>(Try)EscapeDataString<\/code><\/a> and <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.uri.unescapedatastring#system-uri-unescapedatastring%28system-readonlyspan%28%28system-char%29%29%29\"><code>(Try)UnescapeDataString<\/code><\/a> for <code>Uri<\/code> in <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/40603\">dotnet\/runtime#40603<\/a><\/p>\n<p>The goal is to support low-allocation scenarios and we now take advantage of these methods in <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.http.formurlencodedcontent\"><code>FormUrlEncodedContent<\/code><\/a>.<\/p>\n<\/li>\n<li>\n<p>new MIME types for <a href=\"https:\/\/learn.microsoft.com\/dotnet\/api\/system.net.mime.mediatypenames\"><code>MediaTypeNames<\/code><\/a> in <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/95446\">dotnet\/runtime#95446<\/a><\/p>\n<p>These types were collected over the course of the release and implemented in <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/103575\">dotnet\/runtime#103575<\/a> by a community contributor <a href=\"https:\/\/github.com\/CollinAlpert\">@CollinAlpert<\/a>.<\/p>\n<\/li>\n<\/ul>\n<h2>Final Notes<\/h2>\n<p>As each year, we try to write about the interesting and impactful changes in the networking space. This article can&#8217;t possibly cover all the changes that were made. If you are interested, you can find the complete list in our\n<a href=\"https:\/\/github.com\/dotnet\/runtime\/pulls?q=is%3Apr+is%3Aclosed+is%3Amerged+milestone%3A9.0.0+label%3Aarea-System.Net%2Carea-System.Net.Http%2Carea-System.Net.Sockets%2Carea-System.Net.Security+-label%3Atest-enhancement%2Ctest-bug%2Cdisabled-test\">dotnet\/runtime<\/a>\nrepository where you can also reach out to us with question and bugs. On top of that, many of the performance changes that are not mentioned here are in Stephen&#8217;s great article <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/performance-improvements-in-net-9\/#networking\">Performance Improvements in .NET 9<\/a>. We&#8217;d also like to hear from you, so if you encounter an issue or have any feedback, you can file it in <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\">our GitHub repo<\/a>.<\/p>\n<p>Lastly, I&#8217;d like to thank my co-authors:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/antonfirsov\">@antonfirsov<\/a> who wrote <a href=\"#diagnostics\">Diagnostics<\/a>.<\/li>\n<li><a href=\"https:\/\/github.com\/CarnaViire\">@CarnaViire<\/a> who wrote <a href=\"#httpclientfactory\">HttpClientFactory<\/a> and <a href=\"#websockets\">WebSockets<\/a>.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Introducing new networking features in .NET 9 including HTTP space, HttpClientFactory, security and more!<\/p>\n","protected":false},"author":47956,"featured_media":55650,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,7591],"tags":[7797,7676,7898,7899],"class_list":["post-55435","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-networking","tag-dotnet-9","tag-http","tag-http-client-factory","tag-net-security"],"acf":[],"blog_post_summary":"<p>Introducing new networking features in .NET 9 including HTTP space, HttpClientFactory, security and more!<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/55435","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/47956"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=55435"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/55435\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/55650"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=55435"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=55435"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=55435"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}