Skip to content

Commit 093dd61

Browse files
authored
Add IPersistentStateFeature (#34360)
1 parent e2acbb9 commit 093dd61

26 files changed

+540
-51
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.FeatureCollection.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using Microsoft.AspNetCore.Connections;
5+
using Microsoft.AspNetCore.Connections.Features;
46
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
57

68
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http
79
{
810
internal partial class Http1Connection : IHttpMinRequestBodyDataRateFeature,
9-
IHttpMinResponseDataRateFeature
11+
IHttpMinResponseDataRateFeature,
12+
IPersistentStateFeature
1013
{
14+
// Persistent state collection is not reset with a request by design.
15+
// If SocketsConections are pooled in the future this state could be moved
16+
// to the transport layer.
17+
private IDictionary<object, object?>? _persistentState;
18+
1119
MinDataRate? IHttpMinRequestBodyDataRateFeature.MinDataRate
1220
{
1321
get => MinRequestBodyDataRate;
@@ -19,5 +27,14 @@ internal partial class Http1Connection : IHttpMinRequestBodyDataRateFeature,
1927
get => MinResponseDataRate;
2028
set => MinResponseDataRate = value;
2129
}
30+
31+
IDictionary<object, object?> IPersistentStateFeature.State
32+
{
33+
get
34+
{
35+
// Lazily allocate persistent state
36+
return _persistentState ?? (_persistentState = new ConnectionItems());
37+
}
38+
}
2239
}
2340
}

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

Lines changed: 1 addition & 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;
@@ -628,6 +627,7 @@ protected override void OnReset()
628627
// Reset Http1 Features
629628
_currentIHttpMinRequestBodyDataRateFeature = this;
630629
_currentIHttpMinResponseDataRateFeature = this;
630+
_currentIPersistentStateFeature = this;
631631
}
632632

633633
protected override void OnRequestProcessingEnding()

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Runtime.CompilerServices;
88

9+
using Microsoft.AspNetCore.Connections.Features;
910
using Microsoft.AspNetCore.Http.Features;
1011
using Microsoft.AspNetCore.Http.Features.Authentication;
1112
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
@@ -62,6 +63,7 @@ internal partial class HttpProtocol : IFeatureCollection,
6263
internal protected IHttpMinRequestBodyDataRateFeature? _currentIHttpMinRequestBodyDataRateFeature;
6364
internal protected IHttpMinResponseDataRateFeature? _currentIHttpMinResponseDataRateFeature;
6465
internal protected IHttpResetFeature? _currentIHttpResetFeature;
66+
internal protected IPersistentStateFeature? _currentIPersistentStateFeature;
6567

6668
private int _featureRevision;
6769

@@ -99,6 +101,7 @@ private void FastReset()
99101
_currentIHttpMinRequestBodyDataRateFeature = null;
100102
_currentIHttpMinResponseDataRateFeature = null;
101103
_currentIHttpResetFeature = null;
104+
_currentIPersistentStateFeature = null;
102105
}
103106

104107
// Internal for testing
@@ -286,6 +289,10 @@ private void ExtraFeatureSet(Type key, object? value)
286289
{
287290
feature = _currentIHttpResetFeature;
288291
}
292+
else if (key == typeof(IPersistentStateFeature))
293+
{
294+
feature = _currentIPersistentStateFeature;
295+
}
289296
else if (MaybeExtra != null)
290297
{
291298
feature = ExtraFeatureGet(key);
@@ -414,6 +421,10 @@ private void ExtraFeatureSet(Type key, object? value)
414421
{
415422
_currentIHttpResetFeature = (IHttpResetFeature?)value;
416423
}
424+
else if (key == typeof(IPersistentStateFeature))
425+
{
426+
_currentIPersistentStateFeature = (IPersistentStateFeature?)value;
427+
}
417428
else
418429
{
419430
ExtraFeatureSet(key, value);
@@ -544,6 +555,10 @@ private void ExtraFeatureSet(Type key, object? value)
544555
{
545556
feature = Unsafe.As<IHttpResetFeature?, TFeature?>(ref _currentIHttpResetFeature);
546557
}
558+
else if (typeof(TFeature) == typeof(IPersistentStateFeature))
559+
{
560+
feature = Unsafe.As<IPersistentStateFeature?, TFeature?>(ref _currentIPersistentStateFeature);
561+
}
547562
else if (MaybeExtra != null)
548563
{
549564
feature = (TFeature?)(ExtraFeatureGet(typeof(TFeature)));
@@ -680,6 +695,10 @@ private void ExtraFeatureSet(Type key, object? value)
680695
{
681696
_currentIHttpResetFeature = Unsafe.As<TFeature?, IHttpResetFeature?>(ref feature);
682697
}
698+
else if (typeof(TFeature) == typeof(IPersistentStateFeature))
699+
{
700+
_currentIPersistentStateFeature = Unsafe.As<TFeature?, IPersistentStateFeature?>(ref feature);
701+
}
683702
else
684703
{
685704
ExtraFeatureSet(typeof(TFeature), feature);
@@ -804,6 +823,10 @@ private IEnumerable<KeyValuePair<Type, object>> FastEnumerable()
804823
{
805824
yield return new KeyValuePair<Type, object>(typeof(IHttpResetFeature), _currentIHttpResetFeature);
806825
}
826+
if (_currentIPersistentStateFeature != null)
827+
{
828+
yield return new KeyValuePair<Type, object>(typeof(IPersistentStateFeature), _currentIPersistentStateFeature);
829+
}
807830

808831
if (MaybeExtra != null)
809832
{

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/Core/test/Http1HttpProtocolFeatureCollectionTests.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO.Pipelines;
66
using System.Linq;
77
using Microsoft.AspNetCore.Connections;
8+
using Microsoft.AspNetCore.Connections.Features;
89
using Microsoft.AspNetCore.Http.Features;
910
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
1011
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
@@ -123,6 +124,8 @@ public void FeaturesSetByTypeSameAsGeneric()
123124
_collection[typeof(IHttpBodyControlFeature)] = CreateHttp1Connection();
124125
_collection[typeof(IRouteValuesFeature)] = CreateHttp1Connection();
125126
_collection[typeof(IEndpointFeature)] = CreateHttp1Connection();
127+
_collection[typeof(IHttpUpgradeFeature)] = CreateHttp1Connection();
128+
_collection[typeof(IPersistentStateFeature)] = CreateHttp1Connection();
126129

127130
CompareGenericGetterToIndexer();
128131

@@ -147,6 +150,8 @@ public void FeaturesSetByGenericSameAsByType()
147150
_collection.Set<IHttpBodyControlFeature>(CreateHttp1Connection());
148151
_collection.Set<IRouteValuesFeature>(CreateHttp1Connection());
149152
_collection.Set<IEndpointFeature>(CreateHttp1Connection());
153+
_collection.Set<IHttpUpgradeFeature>(CreateHttp1Connection());
154+
_collection.Set<IPersistentStateFeature>(CreateHttp1Connection());
150155

151156
CompareGenericGetterToIndexer();
152157

@@ -190,13 +195,21 @@ private void CompareGenericGetterToIndexer()
190195

191196
private int EachHttpProtocolFeatureSetAndUnique()
192197
{
193-
int featureCount = 0;
198+
var featureCount = 0;
194199
foreach (var item in _collection)
195200
{
196-
Type type = item.Key;
201+
var type = item.Key;
197202
if (type.IsAssignableFrom(typeof(HttpProtocol)))
198203
{
199-
Assert.Equal(1, _collection.Count(kv => ReferenceEquals(kv.Value, item.Value)));
204+
var matches = _collection.Where(kv => ReferenceEquals(kv.Value, item.Value)).ToList();
205+
try
206+
{
207+
Assert.Single(matches);
208+
}
209+
catch (Exception ex)
210+
{
211+
throw new Exception($"Error for feature {type}.", ex);
212+
}
200213

201214
featureCount++;
202215
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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.Net.Sockets;
5+
using Microsoft.AspNetCore.Connections;
6+
using Microsoft.AspNetCore.Connections.Features;
7+
8+
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
9+
{
10+
internal sealed partial class QuicStreamContext : IPersistentStateFeature
11+
{
12+
private IDictionary<object, object?>? _persistentState;
13+
14+
IDictionary<object, object?> IPersistentStateFeature.State
15+
{
16+
get
17+
{
18+
// Lazily allocate persistent state
19+
return _persistentState ?? (_persistentState = new ConnectionItems());
20+
}
21+
}
22+
23+
private void InitializeFeatures()
24+
{
25+
_currentIPersistentStateFeature = this;
26+
}
27+
}
28+
}

src/Servers/Kestrel/Transport.Quic/src/Internal/QuicStreamContext.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.Internal
1818
{
19-
internal class QuicStreamContext : TransportConnection, IStreamDirectionFeature, IProtocolErrorCodeFeature, IStreamIdFeature, IPooledStream
19+
internal partial class QuicStreamContext : TransportConnection, IStreamDirectionFeature, IProtocolErrorCodeFeature, IStreamIdFeature, IPooledStream
2020
{
2121
// Internal for testing.
2222
internal Task _processingTask = Task.CompletedTask;
@@ -87,12 +87,16 @@ public void Initialize(QuicStream stream)
8787
}
8888

8989
ConnectionClosed = _streamClosedTokenSource.Token;
90+
91+
// TODO - add to generated features
9092
Features.Set<IStreamDirectionFeature>(this);
9193
Features.Set<IProtocolErrorCodeFeature>(this);
9294
Features.Set<IStreamIdFeature>(this);
93-
9495
// TODO populate the ITlsConnectionFeature (requires client certs).
9596
Features.Set<ITlsConnectionFeature>(new FakeTlsConnectionFeature());
97+
98+
InitializeFeatures();
99+
96100
CanRead = _stream.CanRead;
97101
CanWrite = _stream.CanWrite;
98102
Error = 0;
@@ -132,6 +136,8 @@ public override string ConnectionId
132136

133137
public void Start()
134138
{
139+
Debug.Assert(_processingTask.IsCompletedSuccessfully);
140+
135141
_processingTask = StartAsync();
136142
}
137143

0 commit comments

Comments
 (0)