Skip to content

Commit bbd90dd

Browse files
committed
Support serializing session changesets
Previously, the serialization of the session state to go between the remote server and the client would deserialize/serialize every key even if it hadn't changed. As part of this change: - The session state tracks which items are new or have been accessed (and thus potentially changed) - Only sends a diff list rather than all the items - Only deserializes items if they are being accessed - Make the ISessionSerializer internal as its not the preferred way to serialize session state (use
1 parent 91322f1 commit bbd90dd

File tree

13 files changed

+954
-157
lines changed

13 files changed

+954
-157
lines changed

designs/session-serialization.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Session serialization
2+
3+
Session serialization is provided through the `ISessionSerializer` type. There are two modes that are available:
4+
5+
## Common structure
6+
7+
```mermaid
8+
packet-beta
9+
0: "M"
10+
1-10: "Session Id (Variable length)"
11+
11: "N"
12+
12: "A"
13+
13: "R"
14+
14: "T"
15+
15: "C"
16+
16-24: "Key 1 Blob"
17+
25-33: "Key 2 Blob"
18+
34-42: "..."
19+
43-50: "Flags (variable)"
20+
```
21+
22+
Where:
23+
- *M*: Mode
24+
- *N*: New session
25+
- *A*: Abandoned
26+
- *R*: Readonly
27+
- *T*: Timeout
28+
- *C*: Key count
29+
30+
## Flags
31+
32+
Flags allow for additional information to be sent either direction that may not be known initially. This field was added v2 but is backwards compatible with the v1 deserializer and will operate as a no-op as it just reads the things it knows about and doesn't look for the end of a payload.
33+
34+
Structure:
35+
36+
```mermaid
37+
packet-beta
38+
0: "C"
39+
1: "F1"
40+
2: "F1L"
41+
3-10: "Flag1 specific payload"
42+
11: "F2"
43+
12: "F2L"
44+
13-20: "Flag2 specific payload"
45+
21-25: "..."
46+
```
47+
48+
Where:
49+
- *Fn*: Flag `n`
50+
51+
Where `C` is the count of flags, and each `Fn` is a flag identifier an int with 7bit encoding. Each f
52+
53+
An example is the flag section used to indicate that there is support for diffing a session state on the server:
54+
55+
```mermaid
56+
packet-beta
57+
0: "1"
58+
1: "100"
59+
2: "0"
60+
```
61+
62+
## Full Copy (Mode = 1)
63+
64+
The following is the structure of the key blobs when the full state is serialized:
65+
66+
```mermaid
67+
packet-beta
68+
0-10: "Key name"
69+
11-20: "Serialized value"
70+
```
71+
72+
## Diffing Support (Mode = 2)
73+
74+
The following is the structure of the key blobs when only the difference is serialized:
75+
76+
```mermaid
77+
packet-beta
78+
0-10: "Key name"
79+
11: "S"
80+
12-20: "Serialized value"
81+
```
82+
83+
Where:
84+
- *S*: A value indicating the change the key has undergone from the values in `SessionItemChangeState`
85+

src/Microsoft.AspNetCore.SystemWebAdapters.Abstractions/SessionState/Serialization/ISessionSerializer.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,21 @@ namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
99

1010
public interface ISessionSerializer
1111
{
12+
/// <summary>
13+
/// Deserializes a session state.
14+
/// </summary>
15+
/// <param name="stream">The serialized session stream.</param>
16+
/// <param name="token">A cancellation token</param>
17+
/// <returns>If the stream defines a serialized session changeset, it will also implement <see cref="ISessionStateChangeset"/>.</returns>
1218
Task<ISessionState?> DeserializeAsync(Stream stream, CancellationToken token);
1319

20+
/// <summary>
21+
/// Serializes the session state. If the <paramref name="state"/> implements <see cref="ISessionStateChangeset"/> it will serialize it
22+
/// in a mode that only tracks the changes that have occurred.
23+
/// </summary>
24+
/// <param name="state"></param>
25+
/// <param name="stream"></param>
26+
/// <param name="token"></param>
27+
/// <returns></returns>
1428
Task SerializeAsync(ISessionState state, Stream stream, CancellationToken token);
1529
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
6+
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
7+
8+
public interface ISessionStateChangeset : ISessionState
9+
{
10+
IEnumerable<SessionStateChangeItem> Changes { get; }
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
5+
6+
public enum SessionItemChangeState
7+
{
8+
Unknown = 0,
9+
NoChange = 1,
10+
Removed = 2,
11+
Changed = 3,
12+
New = 4,
13+
}

src/Microsoft.AspNetCore.SystemWebAdapters.Abstractions/SessionState/Serialization/SessionSerializerOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Licensed to the .NET Foundation under one or more agreements.
1+
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Diagnostics;
6+
7+
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
8+
9+
[DebuggerDisplay("{State}: {Key,nq}")]
10+
public readonly struct SessionStateChangeItem(SessionItemChangeState state, string key) : IEquatable<SessionStateChangeItem>
11+
{
12+
public SessionItemChangeState State => state;
13+
14+
public string Key => key;
15+
16+
public override bool Equals(object? obj) => obj is SessionStateChangeItem item && Equals(item);
17+
18+
public override int GetHashCode()
19+
=> State.GetHashCode() ^ Key.GetHashCode();
20+
21+
public bool Equals(SessionStateChangeItem other) =>
22+
State == other.State
23+
&& string.Equals(Key, other.Key, StringComparison.Ordinal);
24+
25+
public static bool operator ==(SessionStateChangeItem left, SessionStateChangeItem right)
26+
{
27+
return left.Equals(right);
28+
}
29+
30+
public static bool operator !=(SessionStateChangeItem left, SessionStateChangeItem right)
31+
{
32+
return !(left == right);
33+
}
34+
}

src/Microsoft.AspNetCore.SystemWebAdapters.FrameworkServices/SessionState/SessionStateExtensions.cs

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Web;
6+
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
67

78
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;
89

@@ -22,11 +23,43 @@ public static void CopyTo(this ISessionState result, HttpSessionStateBase state)
2223
}
2324

2425
state.Timeout = result.Timeout;
26+
27+
if (result is ISessionStateChangeset changes)
28+
{
29+
UpdateFromChanges(changes, state);
30+
}
31+
else
32+
{
33+
Replace(result, state);
34+
}
35+
}
36+
37+
private static void UpdateFromChanges(ISessionStateChangeset from, HttpSessionStateBase state)
38+
{
39+
foreach (var change in from.Changes)
40+
{
41+
if (change.State is SessionItemChangeState.Changed or SessionItemChangeState.New)
42+
{
43+
state[change.Key] = from[change.Key];
44+
}
45+
else if (change.State is SessionItemChangeState.Removed)
46+
{
47+
state.Remove(change.Key);
48+
}
49+
else if (change.State is SessionItemChangeState.Unknown)
50+
{
51+
52+
}
53+
}
54+
}
55+
56+
private static void Replace(ISessionState from, HttpSessionStateBase state)
57+
{
2558
state.Clear();
2659

27-
foreach (var key in result.Keys)
60+
foreach (var key in from.Keys)
2861
{
29-
state[key] = result[key];
62+
state[key] = from[key];
3063
}
3164
}
3265
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Text;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Microsoft.Extensions.Logging;
12+
using Microsoft.Extensions.Options;
13+
14+
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
15+
16+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "CA1510:Use ArgumentNullException throw helper", Justification = "Source shared with .NET Framework that does not have the method")]
17+
internal partial class BinarySessionSerializer : ISessionSerializer
18+
{
19+
private readonly struct ChangesetWriter(ISessionKeySerializer serializer)
20+
{
21+
public List<string>? Write(ISessionStateChangeset state, BinaryWriter writer)
22+
{
23+
writer.Write(ModeDelta);
24+
writer.Write(state.SessionID);
25+
26+
writer.Write(state.IsNewSession);
27+
writer.Write(state.IsAbandoned);
28+
writer.Write(state.IsReadOnly);
29+
30+
writer.Write7BitEncodedInt(state.Timeout);
31+
writer.Write7BitEncodedInt(state.Count);
32+
33+
List<string>? unknownKeys = null;
34+
35+
foreach (var item in state.Changes)
36+
{
37+
writer.Write(item.Key);
38+
39+
// New with V2 serializer
40+
if (item.State is SessionItemChangeState.NoChange or SessionItemChangeState.Removed)
41+
{
42+
writer.Write7BitEncodedInt((int)item.State);
43+
}
44+
else if (serializer.TrySerialize(item.Key, state[item.Key], out var result))
45+
{
46+
writer.Write7BitEncodedInt((int)item.State);
47+
writer.Write7BitEncodedInt(result.Length);
48+
writer.Write(result);
49+
}
50+
else
51+
{
52+
(unknownKeys ??= []).Add(item.Key);
53+
writer.Write7BitEncodedInt((int)SessionItemChangeState.Unknown);
54+
}
55+
}
56+
57+
writer.WriteFlags([]);
58+
59+
return unknownKeys;
60+
}
61+
62+
public SessionStateCollection Read(BinaryReader reader)
63+
{
64+
var state = SessionStateCollection.CreateTracking(serializer);
65+
66+
state.SessionID = reader.ReadString();
67+
state.IsNewSession = reader.ReadBoolean();
68+
state.IsAbandoned = reader.ReadBoolean();
69+
state.IsReadOnly = reader.ReadBoolean();
70+
state.Timeout = reader.Read7BitEncodedInt();
71+
72+
var count = reader.Read7BitEncodedInt();
73+
74+
for (var index = count; index > 0; index--)
75+
{
76+
var key = reader.ReadString();
77+
var changeState = (SessionItemChangeState)reader.Read7BitEncodedInt();
78+
79+
if (changeState is SessionItemChangeState.NoChange)
80+
{
81+
state.MarkUnchanged(key);
82+
}
83+
else if (changeState is SessionItemChangeState.Removed)
84+
{
85+
state.MarkRemoved(key);
86+
}
87+
else if (changeState is SessionItemChangeState.Unknown)
88+
{
89+
state.AddUnknownKey(key);
90+
}
91+
else if (changeState is SessionItemChangeState.New or SessionItemChangeState.Changed)
92+
{
93+
var length = reader.Read7BitEncodedInt();
94+
var bytes = reader.ReadBytes(length);
95+
96+
if (serializer.TryDeserialize(key, bytes, out var result))
97+
{
98+
if (result is not null)
99+
{
100+
state[key] = result;
101+
}
102+
}
103+
else
104+
{
105+
state.AddUnknownKey(key);
106+
}
107+
}
108+
}
109+
110+
foreach (var (flag, payload) in reader.ReadFlags())
111+
{
112+
// No flags are currently read
113+
}
114+
115+
return state;
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)