Skip to content

Commit 7834169

Browse files
committed
Add PersistentStateFeature
1 parent 9217656 commit 7834169

21 files changed

+512
-42
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.AspNetCore.Connections.Features
7+
{
8+
/// <summary>
9+
/// Provides access to a key/value collection that can be used to persist state between connections and requests.
10+
/// Whether a transport supports persisting state depends on the implementation. The transport must support
11+
/// pooling and reusing connection instances for state to be persisted.
12+
/// <para>
13+
/// Because values added to persistent state can live in memory until a connection is no longer pooled,
14+
/// use caution when adding items to this collection to avoid excessive memory use.
15+
/// </para>
16+
/// </summary>
17+
public interface IPersistentStateFeature
18+
{
19+
/// <summary>
20+
/// Gets a key/value collection that can be used to persist state between connections and requests.
21+
/// </summary>
22+
IDictionary<object, object?> State { get; }
23+
}
24+
}

src/Servers/Connections.Abstractions/src/PublicAPI.Unshipped.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
*REMOVED*Microsoft.AspNetCore.Connections.IConnectionListener.AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext!>
33
Microsoft.AspNetCore.Connections.Features.IConnectionSocketFeature
44
Microsoft.AspNetCore.Connections.Features.IConnectionSocketFeature.Socket.get -> System.Net.Sockets.Socket!
5+
Microsoft.AspNetCore.Connections.Features.IPersistentStateFeature
6+
Microsoft.AspNetCore.Connections.Features.IPersistentStateFeature.State.get -> System.Collections.Generic.IDictionary<object!, object?>!
57
Microsoft.AspNetCore.Connections.IConnectionListener.AcceptAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Connections.ConnectionContext?>
68
Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder
79
Microsoft.AspNetCore.Connections.IMultiplexedConnectionBuilder.ApplicationServices.get -> System.IServiceProvider!

src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,6 @@ private void ValidateNonOriginHostHeader(string hostText)
616616

617617
protected override void OnReset()
618618
{
619-
620619
_requestTimedOut = false;
621620
_requestTargetForm = HttpRequestTarget.Unknown;
622621
_absoluteRequestTarget = null;

src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.Generated.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using System.Collections;
66
using System.Collections.Generic;
77
using System.Runtime.CompilerServices;
8-
8+
using Microsoft.AspNetCore.Connections.Features;
99
using Microsoft.AspNetCore.Http.Features;
1010
using Microsoft.AspNetCore.Http.Features.Authentication;
1111
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
@@ -62,6 +62,7 @@ internal partial class HttpProtocol : IFeatureCollection,
6262
internal protected IHttpMinRequestBodyDataRateFeature? _currentIHttpMinRequestBodyDataRateFeature;
6363
internal protected IHttpMinResponseDataRateFeature? _currentIHttpMinResponseDataRateFeature;
6464
internal protected IHttpResetFeature? _currentIHttpResetFeature;
65+
internal protected IPersistentStateFeature? _currentIPersistentStateFeature;
6566

6667
private int _featureRevision;
6768

@@ -99,6 +100,7 @@ private void FastReset()
99100
_currentIHttpMinRequestBodyDataRateFeature = null;
100101
_currentIHttpMinResponseDataRateFeature = null;
101102
_currentIHttpResetFeature = null;
103+
_currentIPersistentStateFeature = null;
102104
}
103105

104106
// Internal for testing
@@ -286,6 +288,10 @@ private void ExtraFeatureSet(Type key, object? value)
286288
{
287289
feature = _currentIHttpResetFeature;
288290
}
291+
else if (key == typeof(IPersistentStateFeature))
292+
{
293+
feature = _currentIPersistentStateFeature;
294+
}
289295
else if (MaybeExtra != null)
290296
{
291297
feature = ExtraFeatureGet(key);
@@ -414,6 +420,10 @@ private void ExtraFeatureSet(Type key, object? value)
414420
{
415421
_currentIHttpResetFeature = (IHttpResetFeature?)value;
416422
}
423+
else if (key == typeof(IPersistentStateFeature))
424+
{
425+
_currentIPersistentStateFeature = (IPersistentStateFeature?)value;
426+
}
417427
else
418428
{
419429
ExtraFeatureSet(key, value);
@@ -544,6 +554,10 @@ private void ExtraFeatureSet(Type key, object? value)
544554
{
545555
feature = Unsafe.As<IHttpResetFeature?, TFeature?>(ref _currentIHttpResetFeature);
546556
}
557+
else if (typeof(TFeature) == typeof(IPersistentStateFeature))
558+
{
559+
feature = Unsafe.As<IPersistentStateFeature?, TFeature?>(ref _currentIPersistentStateFeature);
560+
}
547561
else if (MaybeExtra != null)
548562
{
549563
feature = (TFeature?)(ExtraFeatureGet(typeof(TFeature)));
@@ -680,6 +694,10 @@ private void ExtraFeatureSet(Type key, object? value)
680694
{
681695
_currentIHttpResetFeature = Unsafe.As<TFeature?, IHttpResetFeature?>(ref feature);
682696
}
697+
else if (typeof(TFeature) == typeof(IPersistentStateFeature))
698+
{
699+
_currentIPersistentStateFeature = Unsafe.As<TFeature?, IPersistentStateFeature?>(ref feature);
700+
}
683701
else
684702
{
685703
ExtraFeatureSet(typeof(TFeature), feature);
@@ -804,6 +822,10 @@ private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
804822
{
805823
yield return new KeyValuePair<Type, object>(typeof(IHttpResetFeature), _currentIHttpResetFeature);
806824
}
825+
if (_currentIPersistentStateFeature != null)
826+
{
827+
yield return new KeyValuePair<Type, object>(typeof(IPersistentStateFeature), _currentIPersistentStateFeature);
828+
}
807829

808830
if (MaybeExtra != null)
809831
{

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.FeatureCollection.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Threading.Tasks;
66
using Microsoft.AspNetCore.Connections;
7+
using Microsoft.AspNetCore.Connections.Features;
78
using Microsoft.AspNetCore.Http;
89
using Microsoft.AspNetCore.Http.Features;
910
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
@@ -14,11 +15,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http2
1415
internal partial class Http2Stream : IHttp2StreamIdFeature,
1516
IHttpMinRequestBodyDataRateFeature,
1617
IHttpResetFeature,
17-
IHttpResponseTrailersFeature
18-
18+
IHttpResponseTrailersFeature,
19+
IPersistentStateFeature
1920
{
2021
private IHeaderDictionary? _userTrailers;
2122

23+
// Persistent state collection is not reset with a stream by design.
24+
private IDictionary<object, object?>? _persistentState;
25+
2226
IHeaderDictionary IHttpResponseTrailersFeature.Trailers
2327
{
2428
get
@@ -65,5 +69,14 @@ void IHttpResetFeature.Reset(int errorCode)
6569
var abortReason = new ConnectionAbortedException(CoreStrings.FormatHttp2StreamResetByApplication((Http2ErrorCode)errorCode));
6670
ApplicationAbort(abortReason, (Http2ErrorCode)errorCode);
6771
}
72+
73+
IDictionary<object, object?> IPersistentStateFeature.State
74+
{
75+
get
76+
{
77+
// Lazily allocate persistent state
78+
return _persistentState ?? (_persistentState = new ConnectionItems());
79+
}
80+
}
6881
}
6982
}

src/Servers/Kestrel/Core/src/Internal/Http2/Http2Stream.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ protected override void OnReset()
119119
_currentIHttp2StreamIdFeature = this;
120120
_currentIHttpResponseTrailersFeature = this;
121121
_currentIHttpResetFeature = this;
122+
_currentIPersistentStateFeature = this;
122123
}
123124

124125
protected override void OnRequestProcessingEnded()

src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Microsoft.AspNetCore.Connections;
1414
using Microsoft.AspNetCore.Connections.Features;
1515
using Microsoft.AspNetCore.Hosting.Server;
16+
using Microsoft.AspNetCore.Http.Features;
1617
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http;
1718
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
1819
using Microsoft.Extensions.Logging;

src/Servers/Kestrel/Transport.Quic/test/QuicConnectionContextTests.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using System.Threading.Tasks;
1111
using Microsoft.AspNetCore.Connections;
1212
using Microsoft.AspNetCore.Connections.Features;
13+
using Microsoft.AspNetCore.Http.Features;
1314
using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests;
1415
using Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal;
1516
using Microsoft.AspNetCore.Testing;
@@ -451,6 +452,68 @@ static async Task SendStream(RequestState requestState)
451452
}
452453
}
453454

455+
[ConditionalFact]
456+
[MsQuicSupported]
457+
public async Task PersistentState_StreamsReused_StatePersisted()
458+
{
459+
// Arrange
460+
await using var connectionListener = await QuicTestHelpers.CreateConnectionListenerFactory(LoggerFactory);
461+
462+
var options = QuicTestHelpers.CreateClientConnectionOptions(connectionListener.EndPoint);
463+
using var clientConnection = new QuicConnection(QuicImplementationProviders.MsQuic, options);
464+
await clientConnection.ConnectAsync().DefaultTimeout();
465+
466+
await using var serverConnection = await connectionListener.AcceptAndAddFeatureAsync().DefaultTimeout();
467+
468+
// Act
469+
var clientStream1 = clientConnection.OpenBidirectionalStream();
470+
await clientStream1.WriteAsync(TestData, endStream: true).DefaultTimeout();
471+
var serverStream1 = await serverConnection.AcceptAsync().DefaultTimeout();
472+
var readResult1 = await serverStream1.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout();
473+
serverStream1.Transport.Input.AdvanceTo(readResult1.Buffer.End);
474+
475+
serverStream1.Features.Get<IPersistentStateFeature>().State["test"] = true;
476+
477+
// Input should be completed.
478+
readResult1 = await serverStream1.Transport.Input.ReadAsync();
479+
Assert.True(readResult1.IsCompleted);
480+
481+
// Complete reading and writing.
482+
await serverStream1.Transport.Input.CompleteAsync();
483+
await serverStream1.Transport.Output.CompleteAsync();
484+
485+
var quicStreamContext1 = Assert.IsType<QuicStreamContext>(serverStream1);
486+
await quicStreamContext1._processingTask.DefaultTimeout();
487+
await quicStreamContext1.DisposeAsync();
488+
489+
var clientStream2 = clientConnection.OpenBidirectionalStream();
490+
await clientStream2.WriteAsync(TestData, endStream: true).DefaultTimeout();
491+
var serverStream2 = await serverConnection.AcceptAsync().DefaultTimeout();
492+
var readResult2 = await serverStream2.Transport.Input.ReadAtLeastAsync(TestData.Length).DefaultTimeout();
493+
serverStream2.Transport.Input.AdvanceTo(readResult2.Buffer.End);
494+
495+
object state = serverStream2.Features.Get<IPersistentStateFeature>().State["test"];
496+
497+
// Input should be completed.
498+
readResult2 = await serverStream2.Transport.Input.ReadAsync();
499+
Assert.True(readResult2.IsCompleted);
500+
501+
// Complete reading and writing.
502+
await serverStream2.Transport.Input.CompleteAsync();
503+
await serverStream2.Transport.Output.CompleteAsync();
504+
505+
var quicStreamContext2 = Assert.IsType<QuicStreamContext>(serverStream2);
506+
await quicStreamContext2._processingTask.DefaultTimeout();
507+
await quicStreamContext2.DisposeAsync();
508+
509+
Assert.Same(quicStreamContext1, quicStreamContext2);
510+
511+
var quicConnectionContext = Assert.IsType<QuicConnectionContext>(serverConnection);
512+
Assert.Equal(1, quicConnectionContext.StreamPool.Count);
513+
514+
Assert.Equal(true, state);
515+
}
516+
454517
private record RequestState(
455518
QuicConnection QuicConnection,
456519
MultiplexedConnectionContext ServerConnection,

src/Servers/Kestrel/shared/KnownHeaders.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ public static string GeneratedFile()
768768
offset += header.BytesCount;
769769
}
770770
}
771-
return $@"// Copyright (c) .NET Foundation. All rights reserved.
771+
var s = $@"// Copyright (c) .NET Foundation. All rights reserved.
772772
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
773773
774774
using System;
@@ -1337,6 +1337,9 @@ public bool MoveNext()
13371337
}}
13381338
}}
13391339
")}}}";
1340+
1341+
// Temporary workaround for https://github.com/dotnet/runtime/issues/55688
1342+
return s.Replace("{{", "{").Replace("}}", "}");
13401343
}
13411344

13421345
private static string GetHeaderLookup()

src/Servers/Kestrel/shared/TransportConnection.FeatureCollection.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO.Pipelines;
77
using System.Threading;
88
using Microsoft.AspNetCore.Connections.Features;
9+
using Microsoft.AspNetCore.Http.Features;
910

1011
#nullable enable
1112

@@ -18,6 +19,8 @@ internal partial class TransportConnection
1819
// and the code generator re-reun, which will change the interface list.
1920
// See also: tools/CodeGenerator/TransportConnectionFeatureCollection.cs
2021

22+
private IDictionary<object, object?>? _persistentState;
23+
2124
MemoryPool<byte> IMemoryPoolFeature.MemoryPool => MemoryPool;
2225

2326
IDuplexPipe IConnectionTransportFeature.Transport
@@ -39,5 +42,14 @@ CancellationToken IConnectionLifetimeFeature.ConnectionClosed
3942
}
4043

4144
void IConnectionLifetimeFeature.Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via IConnectionLifetimeFeature.Abort()."));
45+
46+
IDictionary<object, object?> IPersistentStateFeature.State
47+
{
48+
get
49+
{
50+
// Lazily allocate persistent state
51+
return _persistentState ?? (_persistentState = new ConnectionItems());
52+
}
53+
}
4254
}
4355
}

src/Servers/Kestrel/shared/TransportConnection.Generated.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ internal partial class TransportConnection : IFeatureCollection,
1717
IConnectionIdFeature,
1818
IConnectionTransportFeature,
1919
IConnectionItemsFeature,
20+
IPersistentStateFeature,
2021
IMemoryPoolFeature,
2122
IConnectionLifetimeFeature
2223
{
2324
// Implemented features
2425
internal protected IConnectionIdFeature? _currentIConnectionIdFeature;
2526
internal protected IConnectionTransportFeature? _currentIConnectionTransportFeature;
2627
internal protected IConnectionItemsFeature? _currentIConnectionItemsFeature;
28+
internal protected IPersistentStateFeature? _currentIPersistentStateFeature;
2729
internal protected IMemoryPoolFeature? _currentIMemoryPoolFeature;
2830
internal protected IConnectionLifetimeFeature? _currentIConnectionLifetimeFeature;
2931

@@ -39,6 +41,7 @@ private void FastReset()
3941
_currentIConnectionIdFeature = this;
4042
_currentIConnectionTransportFeature = this;
4143
_currentIConnectionItemsFeature = this;
44+
_currentIPersistentStateFeature = this;
4245
_currentIMemoryPoolFeature = this;
4346
_currentIConnectionLifetimeFeature = this;
4447

@@ -126,6 +129,10 @@ private void ExtraFeatureSet(Type key, object? value)
126129
{
127130
feature = _currentIConnectionItemsFeature;
128131
}
132+
else if (key == typeof(IPersistentStateFeature))
133+
{
134+
feature = _currentIPersistentStateFeature;
135+
}
129136
else if (key == typeof(IMemoryPoolFeature))
130137
{
131138
feature = _currentIMemoryPoolFeature;
@@ -162,6 +169,10 @@ private void ExtraFeatureSet(Type key, object? value)
162169
{
163170
_currentIConnectionItemsFeature = (IConnectionItemsFeature?)value;
164171
}
172+
else if (key == typeof(IPersistentStateFeature))
173+
{
174+
_currentIPersistentStateFeature = (IPersistentStateFeature?)value;
175+
}
165176
else if (key == typeof(IMemoryPoolFeature))
166177
{
167178
_currentIMemoryPoolFeature = (IMemoryPoolFeature?)value;
@@ -200,6 +211,10 @@ private void ExtraFeatureSet(Type key, object? value)
200211
{
201212
feature = Unsafe.As<IConnectionItemsFeature?, TFeature?>(ref _currentIConnectionItemsFeature);
202213
}
214+
else if (typeof(TFeature) == typeof(IPersistentStateFeature))
215+
{
216+
feature = Unsafe.As<IPersistentStateFeature?, TFeature?>(ref _currentIPersistentStateFeature);
217+
}
203218
else if (typeof(TFeature) == typeof(IMemoryPoolFeature))
204219
{
205220
feature = Unsafe.As<IMemoryPoolFeature?, TFeature?>(ref _currentIMemoryPoolFeature);
@@ -239,6 +254,10 @@ private void ExtraFeatureSet(Type key, object? value)
239254
{
240255
_currentIConnectionItemsFeature = Unsafe.As<TFeature?, IConnectionItemsFeature?>(ref feature);
241256
}
257+
else if (typeof(TFeature) == typeof(IPersistentStateFeature))
258+
{
259+
_currentIPersistentStateFeature = Unsafe.As<TFeature?, IPersistentStateFeature?>(ref feature);
260+
}
242261
else if (typeof(TFeature) == typeof(IMemoryPoolFeature))
243262
{
244263
_currentIMemoryPoolFeature = Unsafe.As<TFeature?, IMemoryPoolFeature?>(ref feature);
@@ -271,6 +290,10 @@ private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
271290
{
272291
yield return new KeyValuePair<Type, object>(typeof(IConnectionItemsFeature), _currentIConnectionItemsFeature);
273292
}
293+
if (_currentIPersistentStateFeature != null)
294+
{
295+
yield return new KeyValuePair<Type, object>(typeof(IPersistentStateFeature), _currentIPersistentStateFeature);
296+
}
274297
if (_currentIMemoryPoolFeature != null)
275298
{
276299
yield return new KeyValuePair<Type, object>(typeof(IMemoryPoolFeature), _currentIMemoryPoolFeature);

0 commit comments

Comments
 (0)