James Newton-King 4 лет назад
Родитель
Сommit
093dd618da
26 измененных файлов с 540 добавлено и 51 удалено
  1. 24 0
      src/Servers/Connections.Abstractions/src/Features/IPersistentStateFeature.cs
  2. 2 0
      src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt
  3. 18 1
      src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.FeatureCollection.cs
  4. 1 1
      src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs
  5. 23 0
      src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs
  6. 15 2
      src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.FeatureCollection.cs
  7. 1 0
      src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs
  8. 1 0
      src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
  9. 16 3
      src/Servers/Kestrel/Core/test/Http1HttpProtocolFeatureCollectionTests.cs
  10. 28 0
      src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs
  11. 8 2
      src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs
  12. 64 1
      src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs
  13. 1 1
      src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.FeatureCollection.cs
  14. 1 1
      src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs
  15. 4 1
      src/Servers/Kestrel/shared/KnownHeaders.cs
  16. 2 1
      src/Servers/Kestrel/shared/PooledStreamStack.cs
  17. 1 0
      src/Servers/Kestrel/shared/TransportConnection.FeatureCollection.cs
  18. 22 0
      src/Servers/Kestrel/shared/TransportConnection.Generated.cs
  19. 1 0
      src/Servers/Kestrel/shared/TransportMultiplexedConnection.FeatureCollection.cs
  20. 135 0
      src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs
  21. 63 0
      src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs
  22. 97 34
      src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs
  23. 4 1
      src/Servers/Kestrel/tools/CodeGenerator/FeatureCollectionGenerator.cs
  24. 3 1
      src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
  25. 4 1
      src/Servers/Kestrel/tools/CodeGenerator/ReadOnlySpanStaticDataGenerator.cs
  26. 1 0
      src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs

+ 24 - 0
src/Servers/Connections.Abstractions/src/Features/IPersistentStateFeature.cs

@@ -0,0 +1,24 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Connections.Features
+{
+    /// <summary>
+    /// Provides access to a key/value collection that can be used to persist state between connections and requests.
+    /// Whether a transport supports persisting state depends on the implementation. The transport must support
+    /// pooling and reusing connection instances for state to be persisted.
+    /// <para>
+    /// Because values added to persistent state can live in memory until a connection is no longer pooled,
+    /// use caution when adding items to this collection to avoid excessive memory use.
+    /// </para>
+    /// </summary>
+    public interface IPersistentStateFeature
+    {
+        /// <summary>
+        /// Gets a key/value collection that can be used to persist state between connections and requests.
+        /// </summary>
+        IDictionary<object, object?> State { get; }
+    }
+}

+ 2 - 0
src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt

@@ -2,6 +2,8 @@
 *REMOVED*Microsoft.AspNetCore.Connections.IConnectionListener.AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext!>
 Microsoft.AspNetCore.Connections.Features.IConnectionSocketFeature
 Microsoft.AspNetCore.Connections.Features.IConnectionSocketFeature.Socket.get -> System.Net.Sockets.Socket!
+Microsoft.AspNetCore.Connections.Features.IPersistentStateFeature
+Microsoft.AspNetCore.Connections.Features.IPersistentStateFeature.State.get -> System.Collections.Generic.IDictionary<object!, object?>!
 Microsoft.AspNetCore.Connections.IConnectionListener.AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext?>
 Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder
 Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder.ApplicationServices.get -> System.IServiceProvider!

+ 18 - 1
src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.FeatureCollection.cs

@@ -1,13 +1,21 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Features;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
 {
     internal partial class Http1Connection : IHttpMinRequestBodyDataRateFeature,
-                                             IHttpMinResponseDataRateFeature
+                                             IHttpMinResponseDataRateFeature,
+                                             IPersistentStateFeature
     {
+        // Persistent state collection is not reset with a request by design.
+        // If SocketsConections are pooled in the future this state could be moved
+        // to the transport layer.
+        private IDictionary<object, object?>? _persistentState;
+
         MinDataRate? IHttpMinRequestBodyDataRateFeature.MinDataRate
         {
             get => MinRequestBodyDataRate;
@@ -19,5 +27,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             get => MinResponseDataRate;
             set => MinResponseDataRate = value;
         }
+
+        IDictionary<object, object?> IPersistentStateFeature.State
+        {
+            get
+            {
+                // Lazily allocate persistent state
+                return _persistentState ?? (_persistentState = new ConnectionItems());
+            }
+        }
     }
 }

+ 1 - 1
src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs

@@ -616,7 +616,6 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
 
         protected override void OnReset()
         {
-
             _requestTimedOut = false;
             _requestTargetForm = HttpRequestTarget.Unknown;
             _absoluteRequestTarget = null;
@@ -628,6 +627,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             // Reset Http1 Features
             _currentIHttpMinRequestBodyDataRateFeature = this;
             _currentIHttpMinResponseDataRateFeature = this;
+            _currentIPersistentStateFeature = this;
         }
 
         protected override void OnRequestProcessingEnding()

+ 23 - 0
src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs

@@ -6,6 +6,7 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Runtime.CompilerServices;
 
+using Microsoft.AspNetCore.Connections.Features;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Http.Features.Authentication;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
@@ -62,6 +63,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
         internal protected IHttpMinRequestBodyDataRateFeature? _currentIHttpMinRequestBodyDataRateFeature;
         internal protected IHttpMinResponseDataRateFeature? _currentIHttpMinResponseDataRateFeature;
         internal protected IHttpResetFeature? _currentIHttpResetFeature;
+        internal protected IPersistentStateFeature? _currentIPersistentStateFeature;
 
         private int _featureRevision;
 
@@ -99,6 +101,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             _currentIHttpMinRequestBodyDataRateFeature = null;
             _currentIHttpMinResponseDataRateFeature = null;
             _currentIHttpResetFeature = null;
+            _currentIPersistentStateFeature = null;
         }
 
         // Internal for testing
@@ -286,6 +289,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                 {
                     feature = _currentIHttpResetFeature;
                 }
+                else if (key == typeof(IPersistentStateFeature))
+                {
+                    feature = _currentIPersistentStateFeature;
+                }
                 else if (MaybeExtra != null)
                 {
                     feature = ExtraFeatureGet(key);
@@ -414,6 +421,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
                 {
                     _currentIHttpResetFeature = (IHttpResetFeature?)value;
                 }
+                else if (key == typeof(IPersistentStateFeature))
+                {
+                    _currentIPersistentStateFeature = (IPersistentStateFeature?)value;
+                }
                 else
                 {
                     ExtraFeatureSet(key, value);
@@ -544,6 +555,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             {
                 feature = Unsafe.As<IHttpResetFeature?, TFeature?>(ref _currentIHttpResetFeature);
             }
+            else if (typeof(TFeature) == typeof(IPersistentStateFeature))
+            {
+                feature = Unsafe.As<IPersistentStateFeature?, TFeature?>(ref _currentIPersistentStateFeature);
+            }
             else if (MaybeExtra != null)
             {
                 feature = (TFeature?)(ExtraFeatureGet(typeof(TFeature)));
@@ -680,6 +695,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             {
                 _currentIHttpResetFeature = Unsafe.As<TFeature?, IHttpResetFeature?>(ref feature);
             }
+            else if (typeof(TFeature) == typeof(IPersistentStateFeature))
+            {
+                _currentIPersistentStateFeature = Unsafe.As<TFeature?, IPersistentStateFeature?>(ref feature);
+            }
             else
             {
                 ExtraFeatureSet(typeof(TFeature), feature);
@@ -804,6 +823,10 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
             {
                 yield return new KeyValuePair<Type, object>(typeof(IHttpResetFeature), _currentIHttpResetFeature);
             }
+            if (_currentIPersistentStateFeature != null)
+            {
+                yield return new KeyValuePair<Type, object>(typeof(IPersistentStateFeature), _currentIPersistentStateFeature);
+            }
 
             if (MaybeExtra != null)
             {

+ 15 - 2
src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.FeatureCollection.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Features;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
@@ -14,11 +15,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
     internal partial class Http2Stream : IHttp2StreamIdFeature,
                                          IHttpMinRequestBodyDataRateFeature,
                                          IHttpResetFeature,
-                                         IHttpResponseTrailersFeature
-
+                                         IHttpResponseTrailersFeature,
+                                         IPersistentStateFeature
     {
         private IHeaderDictionary? _userTrailers;
 
+        // Persistent state collection is not reset with a stream by design.
+        private IDictionary<object, object?>? _persistentState;
+
         IHeaderDictionary IHttpResponseTrailersFeature.Trailers
         {
             get
@@ -65,5 +69,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
             var abortReason = new ConnectionAbortedException(CoreStrings.FormatHttp2StreamResetByApplication((Http2ErrorCode)errorCode));
             ApplicationAbort(abortReason, (Http2ErrorCode)errorCode);
         }
+
+        IDictionary<object, object?> IPersistentStateFeature.State
+        {
+            get
+            {
+                // Lazily allocate persistent state
+                return _persistentState ?? (_persistentState = new ConnectionItems());
+            }
+        }
     }
 }

+ 1 - 0
src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs

@@ -119,6 +119,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
             _currentIHttp2StreamIdFeature = this;
             _currentIHttpResponseTrailersFeature = this;
             _currentIHttpResetFeature = this;
+            _currentIPersistentStateFeature = this;
         }
 
         protected override void OnRequestProcessingEnded()

+ 1 - 0
src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

@@ -13,6 +13,7 @@ using System.Threading.Tasks;
 using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Connections.Features;
 using Microsoft.AspNetCore.Hosting.Server;
+using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
 using Microsoft.Extensions.Logging;

+ 16 - 3
src/Servers/Kestrel/Core/test/Http1HttpProtocolFeatureCollectionTests.cs

@@ -5,6 +5,7 @@ using System;
 using System.IO.Pipelines;
 using System.Linq;
 using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Features;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
@@ -123,6 +124,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             _collection[typeof(IHttpBodyControlFeature)] = CreateHttp1Connection();
             _collection[typeof(IRouteValuesFeature)] = CreateHttp1Connection();
             _collection[typeof(IEndpointFeature)] = CreateHttp1Connection();
+            _collection[typeof(IHttpUpgradeFeature)] = CreateHttp1Connection();
+            _collection[typeof(IPersistentStateFeature)] = CreateHttp1Connection();
 
             CompareGenericGetterToIndexer();
 
@@ -147,6 +150,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             _collection.Set<IHttpBodyControlFeature>(CreateHttp1Connection());
             _collection.Set<IRouteValuesFeature>(CreateHttp1Connection());
             _collection.Set<IEndpointFeature>(CreateHttp1Connection());
+            _collection.Set<IHttpUpgradeFeature>(CreateHttp1Connection());
+            _collection.Set<IPersistentStateFeature>(CreateHttp1Connection());
 
             CompareGenericGetterToIndexer();
 
@@ -190,13 +195,21 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
 
         private int EachHttpProtocolFeatureSetAndUnique()
         {
-            int featureCount = 0;
+            var featureCount = 0;
             foreach (var item in _collection)
             {
-                Type type = item.Key;
+                var type = item.Key;
                 if (type.IsAssignableFrom(typeof(HttpProtocol)))
                 {
-                    Assert.Equal(1, _collection.Count(kv => ReferenceEquals(kv.Value, item.Value)));
+                    var matches = _collection.Where(kv => ReferenceEquals(kv.Value, item.Value)).ToList();
+                    try
+                    {
+                        Assert.Single(matches);
+                    }
+                    catch (Exception ex)
+                    {
+                        throw new Exception($"Error for feature {type}.", ex);
+                    }
 
                     featureCount++;
                 }

+ 28 - 0
src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.FeatureCollection.cs

@@ -0,0 +1,28 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Net.Sockets;
+using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Features;
+
+namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
+{
+    internal sealed partial class QuicStreamContext : IPersistentStateFeature
+    {
+        private IDictionary<object, object?>? _persistentState;
+
+        IDictionary<object, object?> IPersistentStateFeature.State
+        {
+            get
+            {
+                // Lazily allocate persistent state
+                return _persistentState ?? (_persistentState = new ConnectionItems());
+            }
+        }
+
+        private void InitializeFeatures()
+        {
+            _currentIPersistentStateFeature = this;
+        }
+    }
+}

+ 8 - 2
src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs

@@ -16,7 +16,7 @@ using Microsoft.Extensions.Logging;
 
 namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
 {
-    internal class QuicStreamContext : TransportConnection, IStreamDirectionFeature, IProtocolErrorCodeFeature, IStreamIdFeature, IPooledStream
+    internal partial class QuicStreamContext : TransportConnection, IStreamDirectionFeature, IProtocolErrorCodeFeature, IStreamIdFeature, IPooledStream
     {
         // Internal for testing.
         internal Task _processingTask = Task.CompletedTask;
@@ -87,12 +87,16 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
             }
 
             ConnectionClosed = _streamClosedTokenSource.Token;
+
+            // TODO - add to generated features
             Features.Set<IStreamDirectionFeature>(this);
             Features.Set<IProtocolErrorCodeFeature>(this);
             Features.Set<IStreamIdFeature>(this);
-
             // TODO populate the ITlsConnectionFeature (requires client certs).
             Features.Set<ITlsConnectionFeature>(new FakeTlsConnectionFeature());
+
+            InitializeFeatures();
+
             CanRead = _stream.CanRead;
             CanWrite = _stream.CanWrite;
             Error = 0;
@@ -132,6 +136,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
 
         public void Start()
         {
+            Debug.Assert(_processingTask.IsCompletedSuccessfully);
+
             _processingTask = StartAsync();
         }
 

+ 64 - 1
src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs

@@ -10,6 +10,7 @@ using System.Text;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Connections;
 using Microsoft.AspNetCore.Connections.Features;
+using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
 using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal;
 using Microsoft.AspNetCore.Testing;
@@ -403,7 +404,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests
                 // TODO: Race condition in QUIC library.
                 // Delay between sending streams to avoid
                 // https://github.com/dotnet/runtime/issues/55249
-                await Task.Delay(50);
+                await Task.Delay(100);
                 streamTasks.Add(SendStream(requestState));
             }
 
@@ -451,6 +452,68 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Tests
             }
         }
 
+        [ConditionalFact]
+        [MsQuicSupported]
+        public async Task PersistentState_StreamsReused_StatePersisted()
+        {
+            // Arrange
+            await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory);
+
+            var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
+            using var clientConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options);
+            await clientConnection.ConnectAsync().DefaultTimeout();
+
+            await using var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
+
+            // Act
+            var clientStream1 = clientConnection.OpenBidirectionalStream();
+            await clientStream1.WriteAsync(TestData, endStream: true).DefaultTimeout();
+            var serverStream1 = await serverConnection.AcceptAsync().DefaultTimeout();
+            var readResult1 = await serverStream1.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout();
+            serverStream1.Transport.Input.AdvanceTo(readResult1.Buffer.End);
+
+            serverStream1.Features.Get<IPersistentStateFeature>().State["test"] = true;
+
+            // Input should be completed.
+            readResult1 = await serverStream1.Transport.Input.ReadAsync();
+            Assert.True(readResult1.IsCompleted);
+
+            // Complete reading and writing.
+            await serverStream1.Transport.Input.CompleteAsync();
+            await serverStream1.Transport.Output.CompleteAsync();
+
+            var quicStreamContext1 = Assert.IsType<QuicStreamContext>(serverStream1);
+            await quicStreamContext1._processingTask.DefaultTimeout();
+            await quicStreamContext1.DisposeAsync();
+
+            var clientStream2 = clientConnection.OpenBidirectionalStream();
+            await clientStream2.WriteAsync(TestData, endStream: true).DefaultTimeout();
+            var serverStream2 = await serverConnection.AcceptAsync().DefaultTimeout();
+            var readResult2 = await serverStream2.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout();
+            serverStream2.Transport.Input.AdvanceTo(readResult2.Buffer.End);
+
+            object state = serverStream2.Features.Get<IPersistentStateFeature>().State["test"];
+
+            // Input should be completed.
+            readResult2 = await serverStream2.Transport.Input.ReadAsync();
+            Assert.True(readResult2.IsCompleted);
+
+            // Complete reading and writing.
+            await serverStream2.Transport.Input.CompleteAsync();
+            await serverStream2.Transport.Output.CompleteAsync();
+
+            var quicStreamContext2 = Assert.IsType<QuicStreamContext>(serverStream2);
+            await quicStreamContext2._processingTask.DefaultTimeout();
+            await quicStreamContext2.DisposeAsync();
+
+            Assert.Same(quicStreamContext1, quicStreamContext2);
+
+            var quicConnectionContext = Assert.IsType<QuicConnectionContext>(serverConnection);
+            Assert.Equal(1, quicConnectionContext.StreamPool.Count);
+
+            Assert.Equal(true, state);
+        }
+
         private record RequestState(
             QuicConnection QuicConnection,
             MultiplexedConnectionContext ServerConnection,

+ 1 - 1
src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.FeatureCollection.cs

@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
     {
         public Socket Socket => _socket;
 
-        private void InitiaizeFeatures()
+        private void InitializeFeatures()
         {
             _currentIConnectionSocketFeature = this;
         }

+ 1 - 1
src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs

@@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal
 
             Transport = new SocketDuplexPipe(this);
 
-            InitiaizeFeatures();
+            InitializeFeatures();
         }
 
         public IDuplexPipe InnerTransport => _originalTransport;

+ 4 - 1
src/Servers/Kestrel/shared/KnownHeaders.cs

@@ -768,7 +768,7 @@ namespace CodeGenerator
                     offset += header.BytesCount;
                 }
             }
-            return $@"// Copyright (c) .NET Foundation. All rights reserved.
+            var s = $@"// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
@@ -1337,6 +1337,9 @@ $@"        private void Clear(long bitsToClear)
         }}
     }}
 ")}}}";
+
+            // Temporary workaround for https://github.com/dotnet/runtime/issues/55688
+            return s.Replace("{{", "{").Replace("}}", "}");
         }
 
         private static string GetHeaderLookup()

+ 2 - 1
src/Servers/Kestrel/shared/PooledStreamStack.cs

@@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel
         void DisposeCore();
     }
 
-    // See https://github.com/dotnet/runtime/blob/master/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/BufferSegmentStack.cs
+    // See https://github.com/dotnet/runtime/blob/da9b16f2804e87c9c1ca9dcd9036e7b53e724f5d/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/BufferSegmentStack.cs
     internal struct PooledStreamStack<TValue> where TValue : class, IPooledStream
     {
         // Internal for testing
@@ -139,6 +139,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel
             return size;
         }
 
+        // See https://github.com/dotnet/runtime/blob/da9b16f2804e87c9c1ca9dcd9036e7b53e724f5d/src/libraries/System.IO.Pipelines/src/System/IO/Pipelines/BufferSegmentStack.cs#L68-L79
         internal readonly struct StreamAsValueType
         {
             private readonly TValue _value;

+ 1 - 0
src/Servers/Kestrel/shared/TransportConnection.FeatureCollection.cs

@@ -6,6 +6,7 @@ using System.Collections.Generic;
 using System.IO.Pipelines;
 using System.Threading;
 using Microsoft.AspNetCore.Connections.Features;
+using Microsoft.AspNetCore.Http.Features;
 
 #nullable enable
 

+ 22 - 0
src/Servers/Kestrel/shared/TransportConnection.Generated.cs

@@ -28,6 +28,7 @@ namespace Microsoft.AspNetCore.Connections
         internal protected IConnectionLifetimeFeature? _currentIConnectionLifetimeFeature;
 
         // Other reserved feature slots
+        internal protected IPersistentStateFeature? _currentIPersistentStateFeature;
         internal protected IConnectionSocketFeature? _currentIConnectionSocketFeature;
 
         private int _featureRevision;
@@ -42,6 +43,7 @@ namespace Microsoft.AspNetCore.Connections
             _currentIMemoryPoolFeature = this;
             _currentIConnectionLifetimeFeature = this;
 
+            _currentIPersistentStateFeature = null;
             _currentIConnectionSocketFeature = null;
         }
 
@@ -126,6 +128,10 @@ namespace Microsoft.AspNetCore.Connections
                 {
                     feature = _currentIConnectionItemsFeature;
                 }
+                else if (key == typeof(IPersistentStateFeature))
+                {
+                    feature = _currentIPersistentStateFeature;
+                }
                 else if (key == typeof(IMemoryPoolFeature))
                 {
                     feature = _currentIMemoryPoolFeature;
@@ -162,6 +168,10 @@ namespace Microsoft.AspNetCore.Connections
                 {
                     _currentIConnectionItemsFeature = (IConnectionItemsFeature?)value;
                 }
+                else if (key == typeof(IPersistentStateFeature))
+                {
+                    _currentIPersistentStateFeature = (IPersistentStateFeature?)value;
+                }
                 else if (key == typeof(IMemoryPoolFeature))
                 {
                     _currentIMemoryPoolFeature = (IMemoryPoolFeature?)value;
@@ -200,6 +210,10 @@ namespace Microsoft.AspNetCore.Connections
             {
                 feature = Unsafe.As<IConnectionItemsFeature?, TFeature?>(ref _currentIConnectionItemsFeature);
             }
+            else if (typeof(TFeature) == typeof(IPersistentStateFeature))
+            {
+                feature = Unsafe.As<IPersistentStateFeature?, TFeature?>(ref _currentIPersistentStateFeature);
+            }
             else if (typeof(TFeature) == typeof(IMemoryPoolFeature))
             {
                 feature = Unsafe.As<IMemoryPoolFeature?, TFeature?>(ref _currentIMemoryPoolFeature);
@@ -239,6 +253,10 @@ namespace Microsoft.AspNetCore.Connections
             {
                 _currentIConnectionItemsFeature = Unsafe.As<TFeature?, IConnectionItemsFeature?>(ref feature);
             }
+            else if (typeof(TFeature) == typeof(IPersistentStateFeature))
+            {
+                _currentIPersistentStateFeature = Unsafe.As<TFeature?, IPersistentStateFeature?>(ref feature);
+            }
             else if (typeof(TFeature) == typeof(IMemoryPoolFeature))
             {
                 _currentIMemoryPoolFeature = Unsafe.As<TFeature?, IMemoryPoolFeature?>(ref feature);
@@ -271,6 +289,10 @@ namespace Microsoft.AspNetCore.Connections
             {
                 yield return new KeyValuePair<Type, object>(typeof(IConnectionItemsFeature), _currentIConnectionItemsFeature);
             }
+            if (_currentIPersistentStateFeature != null)
+            {
+                yield return new KeyValuePair<Type, object>(typeof(IPersistentStateFeature), _currentIPersistentStateFeature);
+            }
             if (_currentIMemoryPoolFeature != null)
             {
                 yield return new KeyValuePair<Type, object>(typeof(IMemoryPoolFeature), _currentIMemoryPoolFeature);

+ 1 - 0
src/Servers/Kestrel/shared/TransportMultiplexedConnection.FeatureCollection.cs

@@ -5,6 +5,7 @@ using System.Buffers;
 using System.Collections.Generic;
 using System.Threading;
 using Microsoft.AspNetCore.Connections.Features;
+using Microsoft.AspNetCore.Http.Features;
 
 namespace Microsoft.AspNetCore.Connections
 {

+ 135 - 0
src/Servers/Kestrel/test/InMemory.FunctionalTests/Http2/Http2ConnectionTests.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Buffers;
+using System.Collections;
 using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
@@ -13,7 +14,9 @@ using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Features;
 using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2;
@@ -208,6 +211,123 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
         }
 
+        private class ResponseTrailersWrapper : IHeaderDictionary
+        {
+            readonly IHeaderDictionary _innerHeaders;
+
+            public ResponseTrailersWrapper(IHeaderDictionary headers)
+            {
+                _innerHeaders = headers;
+            }
+
+            public StringValues this[string key] { get => _innerHeaders[key]; set => _innerHeaders[key] = value; }
+            public long? ContentLength { get => _innerHeaders.ContentLength; set => _innerHeaders.ContentLength = value; }
+            public ICollection<string> Keys => _innerHeaders.Keys;
+            public ICollection<StringValues> Values => _innerHeaders.Values;
+            public int Count => _innerHeaders.Count;
+            public bool IsReadOnly => _innerHeaders.IsReadOnly;
+            public void Add(string key, StringValues value) => _innerHeaders.Add(key, value);
+            public void Add(KeyValuePair<string, StringValues> item) => _innerHeaders.Add(item);
+            public void Clear() => _innerHeaders.Clear();
+            public bool Contains(KeyValuePair<string, StringValues> item) => _innerHeaders.Contains(item);
+            public bool ContainsKey(string key) => _innerHeaders.ContainsKey(key);
+            public void CopyTo(KeyValuePair<string, StringValues>[] array, int arrayIndex) => _innerHeaders.CopyTo(array, arrayIndex);
+            public IEnumerator<KeyValuePair<string, StringValues>> GetEnumerator() => _innerHeaders.GetEnumerator();
+            public bool Remove(string key) => _innerHeaders.Remove(key);
+            public bool Remove(KeyValuePair<string, StringValues> item) => _innerHeaders.Remove(item);
+            public bool TryGetValue(string key, out StringValues value) => _innerHeaders.TryGetValue(key, out value);
+            IEnumerator IEnumerable.GetEnumerator() => _innerHeaders.GetEnumerator();
+        }
+
+        [Fact]
+        public async Task ResponseTrailers_MultipleStreams_Reset()
+        {
+            IEnumerable<KeyValuePair<string, string>> requestHeaders = new[]
+            {
+                new KeyValuePair<string, string>(HeaderNames.Method, "GET"),
+                new KeyValuePair<string, string>(HeaderNames.Path, "/hello"),
+                new KeyValuePair<string, string>(HeaderNames.Scheme, "http"),
+                new KeyValuePair<string, string>(HeaderNames.Authority, "localhost:80"),
+                new KeyValuePair<string, string>(HeaderNames.ContentType, "application/json")
+            };
+
+            var requestCount = 0;
+            await InitializeConnectionAsync(context =>
+            {
+                requestCount++;
+
+                var trailersFeature = context.Features.Get<IHttpResponseTrailersFeature>();
+
+                IHeaderDictionary trailers;
+                if (requestCount == 1)
+                {
+                    trailers = new ResponseTrailersWrapper(trailersFeature.Trailers);
+                    trailersFeature.Trailers = trailers;
+                }
+                else
+                {
+                    trailers = trailersFeature.Trailers;
+                }
+                trailers["trailer-" + requestCount] = "true";
+                return Task.CompletedTask;
+            });
+
+            await StartStreamAsync(1, requestHeaders, endStream: true);
+
+            await ExpectAsync(Http2FrameType.HEADERS,
+                withLength: 36,
+                withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
+                withStreamId: 1);
+
+            var trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+                withLength: 16,
+                withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+                withStreamId: 1);
+
+            _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
+
+            Assert.Single(_decodedHeaders);
+            Assert.Equal("true", _decodedHeaders["trailer-1"]);
+
+            _decodedHeaders.Clear();
+
+            // Ping will trigger the stream to be returned to the pool so we can assert it
+            await SendPingAsync(Http2PingFrameFlags.NONE);
+            await ExpectAsync(Http2FrameType.PING,
+                withLength: 8,
+                withFlags: (byte)Http2PingFrameFlags.ACK,
+                withStreamId: 0);
+            await SendPingAsync(Http2PingFrameFlags.NONE);
+            await ExpectAsync(Http2FrameType.PING,
+                withLength: 8,
+                withFlags: (byte)Http2PingFrameFlags.ACK,
+                withStreamId: 0);
+
+            // Stream has been returned to the pool
+            Assert.Equal(1, _connection.StreamPool.Count);
+
+            await StartStreamAsync(3, requestHeaders, endStream: true);
+
+            await ExpectAsync(Http2FrameType.HEADERS,
+                withLength: 6,
+                withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS),
+                withStreamId: 3);
+
+            trailersFrame = await ExpectAsync(Http2FrameType.HEADERS,
+                withLength: 16,
+                withFlags: (byte)(Http2HeadersFrameFlags.END_HEADERS | Http2HeadersFrameFlags.END_STREAM),
+                withStreamId: 3);
+
+            _hpackDecoder.Decode(trailersFrame.PayloadSequence, endHeaders: true, handler: this);
+
+            Assert.Single(_decodedHeaders);
+            Assert.Equal("true", _decodedHeaders["trailer-2"]);
+
+            _decodedHeaders.Clear();
+
+            await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
+        }
+
         [Fact]
         public async Task StreamPool_SingleStream_ReturnedToPool()
         {
@@ -304,9 +424,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
         public async Task StreamPool_MultipleStreamsInSequence_PooledStreamReused()
         {
             TaskCompletionSource appDelegateTcs = null;
+            object persistedState = null;
+            var requestCount = 0;
 
             await InitializeConnectionAsync(async context =>
             {
+                requestCount++;
+                var persistentStateCollection = context.Features.Get<IPersistentStateFeature>().State;
+                if (persistentStateCollection.TryGetValue("Counter", out var value))
+                {
+                    persistedState = value;
+                }
+                persistentStateCollection["Counter"] = requestCount;
                 await appDelegateTcs.Task;
             });
 
@@ -330,6 +459,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             Assert.True(_connection.StreamPool.TryPeek(out var pooledStream));
             Assert.Equal(stream, pooledStream);
 
+            // First request has no persisted state
+            Assert.Null(persistedState);
+
             appDelegateTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
             await StartStreamAsync(3, _browserRequestHeaders, endStream: true);
 
@@ -348,6 +480,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests
             Assert.True(_connection.StreamPool.TryPeek(out pooledStream));
             Assert.Equal(stream, pooledStream);
 
+            // State persisted on first request was available on the second request
+            Assert.Equal(1, (int)persistedState);
+
             await StopConnectionAsync(expectedLastStreamId: 3, ignoreNonGoAwayFrames: false);
 
             async Task PingUntilStreamPooled(int expectedCount)

+ 63 - 0
src/Servers/Kestrel/test/InMemory.FunctionalTests/RequestTests.cs

@@ -9,6 +9,7 @@ using System.Linq;
 using System.Text;
 using System.Threading;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Connections.Features;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
@@ -2055,6 +2056,68 @@ namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests
             }
         }
 
+        [Fact]
+        public async Task PersistentStateBetweenRequests()
+        {
+            var testContext = new TestServiceContext(LoggerFactory);
+            object persistedState = null;
+            var requestCount = 0;
+
+            await using (var server = new TestServer(context =>
+            {
+                requestCount++;
+                var persistentStateCollection = context.Features.Get<IPersistentStateFeature>().State;
+                if (persistentStateCollection.TryGetValue("Counter", out var value))
+                {
+                    persistedState = value;
+                }
+                persistentStateCollection["Counter"] = requestCount;
+                return Task.CompletedTask;
+            }, testContext))
+            {
+                using (var connection = server.CreateConnection())
+                {
+                    // First request
+                    await connection.Send(
+                        "GET / HTTP/1.1",
+                        "Host:",
+                        "Content-Type: application/test",
+                        "X-CustomHeader: customvalue",
+                        "",
+                        "");
+                    await connection.Receive(
+                        "HTTP/1.1 200 OK",
+                        "Content-Length: 0",
+                        $"Date: {testContext.DateHeaderValue}",
+                        "",
+                        "");
+                    var firstRequestState = persistedState;
+
+                    // Second request
+                    await connection.Send(
+                        "GET / HTTP/1.1",
+                        "Host:",
+                        "Content-Type: application/test",
+                        "X-CustomHeader: customvalue",
+                        "",
+                        "");
+                    await connection.Receive(
+                        "HTTP/1.1 200 OK",
+                        "Content-Length: 0",
+                        $"Date: {testContext.DateHeaderValue}",
+                        "",
+                        "");
+                    var secondRequestState = persistedState;
+
+                    // First request has no persisted state
+                    Assert.Null(firstRequestState);
+
+                    // State persisted on first request was available on the second request
+                    Assert.Equal(1, secondRequestState);
+                }
+            }
+        }
+
         [Fact]
         public async Task Latin1HeaderValueAcceptedWhenLatin1OptionIsConfigured()
         {

+ 97 - 34
src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3RequestTests.cs

@@ -6,6 +6,7 @@ using System.Net.Http;
 using System.Text;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Connections;
+using Microsoft.AspNetCore.Connections.Features;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
@@ -54,41 +55,24 @@ namespace Interop.FunctionalTests.Http3
         public async Task POST_ServerCompletsWithoutReadingRequestBody_ClientGetsResponse()
         {
             // Arrange
-            var builder = GetHostBuilder()
-                .ConfigureWebHost(webHostBuilder =>
+            var builder = CreateHttp3HostBuilder(async context =>
+            {
+                var body = context.Request.Body;
+
+                var data = new List<byte>();
+                var buffer = new byte[1024];
+                var readCount = 0;
+                while ((readCount = await body.ReadAsync(buffer).DefaultTimeout()) != -1)
                 {
-                    webHostBuilder
-                        .UseKestrel(o =>
-                        {
-                            o.ConfigureEndpointDefaults(listenOptions =>
-                            {
-                                listenOptions.Protocols = HttpProtocols.Http3;
-                            });
-                        })
-                        .UseUrls("https://127.0.0.1:0")
-                        .Configure(app =>
-                        {
-                            app.Run(async context =>
-                            {
-                                var body = context.Request.Body;
-
-                                var data = new List<byte>();
-                                var buffer = new byte[1024];
-                                var readCount = 0;
-                                while ((readCount = await body.ReadAsync(buffer).DefaultTimeout()) != -1)
-                                {
-                                    data.AddRange(buffer.AsMemory(0, readCount).ToArray());
-                                    if (data.Count == TestData.Length)
-                                    {
-                                        break;
-                                    }
-                                }
-
-                                await context.Response.Body.WriteAsync(buffer.AsMemory(0, TestData.Length));
-                            });
-                        });
-                })
-                .ConfigureServices(AddTestLogging);
+                    data.AddRange(buffer.AsMemory(0, readCount).ToArray());
+                    if (data.Count == TestData.Length)
+                    {
+                        break;
+                    }
+                }
+
+                await context.Response.Body.WriteAsync(buffer.AsMemory(0, TestData.Length));
+            });
 
             using (var host = builder.Build())
             using (var client = new HttpClient())
@@ -124,6 +108,85 @@ namespace Interop.FunctionalTests.Http3
             }
         }
 
+        [ConditionalFact]
+        [MsQuicSupported]
+        public async Task GET_MultipleRequestsInSequence_ReusedState()
+        {
+            // Arrange
+            object persistedState = null;
+            var requestCount = 0;
+
+            var builder = CreateHttp3HostBuilder(context =>
+            {
+                requestCount++;
+                var persistentStateCollection = context.Features.Get<IPersistentStateFeature>().State;
+                if (persistentStateCollection.TryGetValue("Counter", out var value))
+                {
+                    persistedState = value;
+                }
+                persistentStateCollection["Counter"] = requestCount;
+
+                return Task.CompletedTask;
+            });
+
+            using (var host = builder.Build())
+            using (var client = new HttpClient())
+            {
+                await host.StartAsync();
+
+                // Act
+                var request1 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/");
+                request1.Version = HttpVersion.Version30;
+                request1.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
+
+                var response1 = await client.SendAsync(request1);
+                response1.EnsureSuccessStatusCode();
+                var firstRequestState = persistedState;
+
+                // Delay to ensure the stream has enough time to return to pool
+                await Task.Delay(100);
+
+                var request2 = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/");
+                request2.Version = HttpVersion.Version30;
+                request2.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
+
+                var response2 = await client.SendAsync(request2);
+                response2.EnsureSuccessStatusCode();
+                var secondRequestState = persistedState;
+
+                // Assert
+                // First request has no persisted state
+                Assert.Null(firstRequestState);
+
+                // State persisted on first request was available on the second request
+                Assert.Equal(1, secondRequestState);
+
+                await host.StopAsync();
+            }
+        }
+
+        private IHostBuilder CreateHttp3HostBuilder(RequestDelegate requestDelegate)
+        {
+            return GetHostBuilder()
+                .ConfigureWebHost(webHostBuilder =>
+                {
+                    webHostBuilder
+                        .UseKestrel(o =>
+                        {
+                            o.ConfigureEndpointDefaults(listenOptions =>
+                            {
+                                listenOptions.Protocols = HttpProtocols.Http3;
+                            });
+                        })
+                        .UseUrls("https://127.0.0.1:0")
+                        .Configure(app =>
+                        {
+                            app.Run(requestDelegate);
+                        });
+                })
+                .ConfigureServices(AddTestLogging);
+        }
+
         public static IHostBuilder GetHostBuilder(long? maxReadBufferSize = null)
         {
             return new HostBuilder()

+ 4 - 1
src/Servers/Kestrel/tools/CodeGenerator/FeatureCollectionGenerator.cs

@@ -19,7 +19,7 @@ namespace CodeGenerator
                 Index = index
             });
 
-            return $@"// Copyright (c) .NET Foundation. All rights reserved.
+            var s = $@"// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
@@ -211,6 +211,9 @@ namespace {namespaceName}
     }}
 }}
 ";
+
+            // Temporary workaround for https://github.com/dotnet/runtime/issues/55688
+            return s.Replace("{{", "{").Replace("}}", "}");
         }
 
         static string Each<T>(IEnumerable<T> values, Func<T, string> formatter)

+ 3 - 1
src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs

@@ -50,7 +50,8 @@ namespace CodeGenerator
                 "IHttpMinResponseDataRateFeature",
                 "IHttpBodyControlFeature",
                 "IHttpRequestBodyDetectionFeature",
-                "IHttpResetFeature"
+                "IHttpResetFeature",
+                "IPersistentStateFeature"
             };
 
             var allFeatures = alwaysFeatures
@@ -80,6 +81,7 @@ namespace CodeGenerator
             };
 
             var usings = $@"
+using Microsoft.AspNetCore.Connections.Features;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.Http.Features.Authentication;
 using Microsoft.AspNetCore.Server.Kestrel.Core.Features;";

+ 4 - 1
src/Servers/Kestrel/tools/CodeGenerator/ReadOnlySpanStaticDataGenerator.cs

@@ -19,7 +19,7 @@ namespace CodeGenerator
                 Index = index
             });
 
-            return $@"// Copyright (c) .NET Foundation. All rights reserved.
+            var s = $@"// Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
 using System;
@@ -36,6 +36,9 @@ namespace {namespaceName}
     }}
 }}
 ";
+
+            // Temporary workaround for https://github.com/dotnet/runtime/issues/55688
+            return s.Replace("{{", "{").Replace("}}", "}");
         }
 
         private static string Each<T>(IEnumerable<T> values, Func<T, string> formatter)

+ 1 - 0
src/Servers/Kestrel/tools/CodeGenerator/TransportConnectionFeatureCollection.cs

@@ -17,6 +17,7 @@ namespace CodeGenerator
                 "IConnectionIdFeature",
                 "IConnectionTransportFeature",
                 "IConnectionItemsFeature",
+                "IPersistentStateFeature",
                 "IMemoryPoolFeature",
                 "IConnectionLifetimeFeature",
                 "IConnectionSocketFeature"