Skip to content

Commit 49ea3bd

Browse files
authored
Implement ISessionState against ISession (#21)
1 parent 96b0a9a commit 49ea3bd

32 files changed

+955
-65
lines changed
-7.42 KB
Binary file not shown.

docs/session-state/remote-session.md

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,50 @@ In order to configure it, both the framework and core app must set an API key as
88

99
- `ApiKeyHeader` - header name that will contain an API key to secure the endpoint added on .NET Framework
1010
- `ApiKey` - the shared API key that will be validated in the .NET Framework handler
11-
- `RegisterKey<T>(string)` - Registers a session key to a known type. This is required in order to serialize/deserialize the session state correctly. If a key is found that there is no registration for, an error will be thrown and session will not be available.
1211

1312
Configuration for ASP.NET Core would look similar to the following:
1413

1514
```csharp
1615
builder.Services.AddSystemWebAdapters()
16+
.AddJsonSessionSerializer(options =>
17+
{
18+
options.RegisterKey<int>("test-value");
19+
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
20+
})
1721
.AddRemoteAppSession(options =>
1822
{
1923
options.RemoteApp = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);
2024
options.ApiKey = "test-key";
21-
options.RegisterKey<int>("test-value");
22-
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
2325
});
2426
```
2527

2628
The framework equivalent would look like the following change in `Global.asax.cs`:
2729

2830
```csharp
2931
Application.AddSystemWebAdapters()
30-
.AddRemoteAppSession(options=>
31-
{
32-
options.ApiKey = "test-key";
33-
options.RegisterKey<int>("test-value");
34-
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
35-
});
32+
.AddRemoteAppSession(
33+
options => options.ApiKey = "test-key",
34+
options =>
35+
{
36+
options.RegisterKey<int>("test-value");
37+
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
38+
});
3639
```
3740
# Protocol
3841

3942
## Readonly
4043
Readonly session will retrieve the session state from the framework app without any sort of locking. This consists of a single `GET` request that will return a session state and can be closed immediately.
4144

42-
![Readonly protocol](./readonly-remote-session.png)
45+
```mermaid
46+
sequenceDiagram
47+
participant core as ASP.NET Core
48+
participant framework as ASP.NET
49+
participant session as Session Store
50+
core ->> framework: GET /session
51+
framework ->> session: Request session
52+
session -->> framework: Session
53+
framework -->> core: Session
54+
```
4355

4456
## Writeable
4557

@@ -48,4 +60,17 @@ Writeable session state protocol starts with the the same as the readonly, but d
4860
- Requires an additional `PUT` request to update the state
4961
- The initial `GET` request must be kept open until the session is done; if closed, the session will not be able to be updated
5062

51-
![Writeable protocl](writeable-remote-session.png)
63+
```mermaid
64+
sequenceDiagram
65+
participant core as ASP.NET Core
66+
participant framework as ASP.NET
67+
participant session as Session Store
68+
core ->> framework: GET /session
69+
framework ->> session: Request session
70+
session -->> framework: Session
71+
framework -->> core: Session
72+
core ->> framework: PUT /session
73+
framework ->> framework: Deserialize to HttpSessionState
74+
framework -->> core: Session complete
75+
framework ->> session: Persist
76+
```

docs/session-state/session.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Session State
2+
3+
Session state in ASP.NET Framework provided a number of features that ASP.NET Core does not provide. In order to migrate from ASP.NET Framework to Core, the adapters provide mechanisms to enable populating session state with similar behavior as `System.Web` did. Some of the differences between framework and core are:
4+
5+
- ASP.NET Framework would lock session usage within a session, so subsequent requests in a session are handled in a serial fashion. This is different than ASP.NET Core that does not provide any of these guarantees.
6+
- ASP.NET Framework would serialize and deserialize objects automatically (unless being done in-memory). ASP.NET Core simply provides a mechanism to store a `byte[]` given a key. Any object serialization/deserialization has to be done manually be the user.
7+
8+
The adapter infrastructure exposes two interfaces that can be used to implement any session storage system. These are:
9+
10+
- `Microsoft.AspNetCore.SystemWebAdapters.ISessionManager`: This has a single method that gets passed an `HttpContext` and the session metadata and expects an `ISessionState` object to be returned.
11+
- `Microsoft.AspNetCore.SystemWebAdapters.ISessionState`: This describes the state of a session object. It is used as the backing of the `System.Web.SessionState.HttpSessionState` type.
12+
13+
## Serialization
14+
Since the adapters provide the ability to work with strongly-typed session state, we must be able to serialize and deserialize types. This is accomplished through implementation of the type `Microsoft.AspnetCore.SysteWebAdapters.SessionState.Serialization.ISessionSerializer`, of which a JSON implementation is provided.
15+
16+
Serialization and deserialization of session keys requires additional information which is configured via the `SessionSerializerOptions`:
17+
18+
- `RegisterKey<T>(string)` - Registers a session key to a known type. This is required in order to serialize/deserialize the session state correctly. If a key is found that there is no registration for, an error will be thrown and session will not be available.
19+
20+
To use the default JSON backed implementation, add the following to the startup:
21+
22+
```csharp
23+
builder.Services.AddSystemWebAdapters()
24+
.AddJsonSessionSerializer(options =>
25+
{
26+
options.RegisterKey<int>("test-value");
27+
});
28+
```
29+
30+
## Implementations
31+
32+
There are two available implementations of the session state object that currently ship, each with some trade offs of features. The best choice for an application may depend on which part of the migration it is in, and may change over time.
33+
34+
- Strongly typed: Provides the ability to access an object and can be cast to the expected type
35+
- Locking: Ensures multiple requests within a single session are queued up and aren't accessing the session at the same time
36+
- Standalone: Can be used when there is just a .NET Core app without needing additional support.
37+
38+
Below are the available implementations:
39+
40+
| Implementation | Strongly typed | Locking | Standalone |
41+
|-------------------------------------------------------------|----------------|---------|------------|
42+
| [Remote app](remote-session.md) | ✔️ | ✔️ ||
43+
| [Wrapped ASP.NET Core](wrapped-aspnetcore-session.md) | ✔️ || ✔️ |
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Wrapped ASP.NET Core Session State
2+
3+
This implementation wraps the session provided on ASP.NET Core so that it can be used with the adapters. The session will be using the same backing store as `Microsoft.AspNetCore.Http.ISession` but will provide strongly-typed access to its members.
4+
5+
Configuration for ASP.NET Core would look similar to the following:
6+
7+
```csharp
8+
builder.Services.AddSystemWebAdapters()
9+
.AddJsonSessionSerializer(options =>
10+
{
11+
options.RegisterKey<int>("test-value");
12+
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
13+
})
14+
.WrapAspNetCoreSession();
15+
```
16+
17+
The framework app would not need any changes to enable this behavior.
-9.54 KB
Binary file not shown.

samples/ClassLibrary/SessionUtils.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
using Microsoft.AspNetCore.SystemWebAdapters.SessionState;
1+
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
22

33
namespace ClassLibrary;
44

55
public class SessionUtils
66
{
77
public static string ApiKey = "test-key";
88

9-
public static void RegisterSessionKeys(SessionOptions options)
9+
public static void RegisterSessionKeys(SessionSerializerOptions options)
1010
{
1111
options.RegisterKey<int>("test-value");
1212
options.RegisterKey<SessionDemoModel>("SampleSessionItem");

samples/MvcApp/Global.asax.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,9 @@ protected void Application_Start()
1818

1919
Application.AddSystemWebAdapters()
2020
.AddProxySupport(options => options.UseForwardedHeaders = true)
21-
.AddRemoteAppSession(options=>
22-
{
23-
options.ApiKey = ClassLibrary.SessionUtils.ApiKey;
24-
ClassLibrary.SessionUtils.RegisterSessionKeys(options);
25-
});
21+
.AddRemoteAppSession(
22+
options => options.ApiKey = ClassLibrary.SessionUtils.ApiKey,
23+
options => ClassLibrary.SessionUtils.RegisterSessionKeys(options));
2624
}
2725
}
2826
}

samples/MvcCoreApp/Program.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,11 @@
66
// Add services to the container.
77
builder.Services.AddControllersWithViews();
88
builder.Services.AddSystemWebAdapters()
9+
.AddJsonSessionSerializer(options => ClassLibrary.SessionUtils.RegisterSessionKeys(options))
910
.AddRemoteAppSession(options =>
1011
{
1112
options.RemoteApp = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);
1213
options.ApiKey = ClassLibrary.SessionUtils.ApiKey;
13-
14-
ClassLibrary.SessionUtils.RegisterSessionKeys(options);
1514
});
1615

1716
var app = builder.Build();
@@ -36,8 +35,8 @@
3635
app.UseEndpoints(endpoints =>
3736
{
3837
app.MapDefaultControllerRoute();
39-
// This method can be used to enable session (or read-only session) on all controllers
40-
//.RequireSystemWebAdapterSession();
38+
// This method can be used to enable session (or read-only session) on all controllers
39+
//.RequireSystemWebAdapterSession();
4140

4241
app.MapReverseProxy();
4342
});

src/Microsoft.AspNetCore.SystemWebAdapters.SessionState/Microsoft.AspNetCore.SystemWebAdapters.SessionState.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks>net6.0;netcoreapp3.1;net472</TargetFrameworks>
@@ -42,7 +42,7 @@
4242
<Compile Include="RemoteSession/RemoteAppSessionStateOptions.cs" />
4343
<Compile Include="Serialization/SessionValues.cs" />
4444
<Compile Include="Serialization/SerializedSessionState.cs" />
45-
<Compile Include="Serialization/SessionSerializer.Shared.cs" />
45+
<Compile Include="Serialization/JsonSessionSerializer.Shared.cs" />
4646

4747
<Reference Include="System.Web" />
4848

src/Microsoft.AspNetCore.SystemWebAdapters.SessionState/RemoteSession/RemoteAppSessionStateExtensions.Framework.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,22 @@
33

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

78
namespace Microsoft.AspNetCore.SystemWebAdapters;
89

910
public static class RemoteAppSessionStateExtensions
1011
{
11-
public static ISystemWebAdapterBuilder AddRemoteAppSession(this ISystemWebAdapterBuilder builder, Action<RemoteAppSessionStateOptions> configure)
12+
public static ISystemWebAdapterBuilder AddRemoteAppSession(this ISystemWebAdapterBuilder builder, Action<RemoteAppSessionStateOptions> configureRemote, Action<SessionSerializerOptions> configureSerializer)
1213
{
1314
var options = new RemoteAppSessionStateOptions();
14-
configure(options);
15-
builder.Modules.Add(new RemoteSessionModule(options));
15+
configureRemote(options);
16+
17+
var serializerOptions = new SessionSerializerOptions();
18+
configureSerializer(serializerOptions);
19+
var serializer = new JsonSessionSerializer(serializerOptions.KnownKeys);
20+
21+
builder.Modules.Add(new RemoteSessionModule(options, serializer));
1622
return builder;
1723
}
1824
}

src/Microsoft.AspNetCore.SystemWebAdapters.SessionState/RemoteSession/RemoteAppSessionStateExtensions.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System;
55
using System.Net.Http;
66
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;
7-
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
87
using Microsoft.Extensions.DependencyInjection;
98

109
namespace Microsoft.AspNetCore.SystemWebAdapters;
@@ -13,7 +12,6 @@ public static class RemoteAppSessionStateExtensions
1312
{
1413
public static ISystemWebAdapterBuilder AddRemoteAppSession(this ISystemWebAdapterBuilder builder, Action<RemoteAppSessionStateOptions> configure)
1514
{
16-
builder.Services.AddSingleton<ISessionSerializer, SessionSerializer>();
1715
builder.Services.AddHttpClient<ISessionManager, RemoteAppSessionStateManager>()
1816
// Disable cookies in the HTTP client because the service will manage the cookie header directly
1917
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { UseCookies = false });

src/Microsoft.AspNetCore.SystemWebAdapters.SessionState/RemoteSession/RemoteAppSessionStateHandler.Framework.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,22 @@ namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;
1515
internal sealed class RemoteAppSessionStateHandler : HttpTaskAsyncHandler
1616
{
1717
private readonly RemoteAppSessionStateOptions _options;
18-
private readonly SessionSerializer _serializer;
18+
private readonly ISessionSerializer _serializer;
1919

2020
// Track locked sessions awaiting updates or release
2121
private static readonly ConcurrentDictionary<string, SessionContainer> SessionResponseTasks = new();
2222

2323
public override bool IsReusable => true;
2424

25-
public RemoteAppSessionStateHandler(RemoteAppSessionStateOptions options)
25+
public RemoteAppSessionStateHandler(RemoteAppSessionStateOptions options, ISessionSerializer serializer)
2626
{
2727
if (string.IsNullOrEmpty(options.ApiKey))
2828
{
2929
throw new ArgumentOutOfRangeException("API key must not be empty.");
3030
}
3131

3232
_options = options;
33-
_serializer = new SessionSerializer(options.KnownKeys);
33+
_serializer = serializer;
3434
}
3535

3636
public override async Task ProcessRequestAsync(HttpContext context)

src/Microsoft.AspNetCore.SystemWebAdapters.SessionState/RemoteSession/RemoteAppSessionStateOptions.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
using System.ComponentModel.DataAnnotations;
66
#endif
77

8-
using System.Collections.Generic;
98
using System;
109

1110
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;
1211

13-
public class RemoteAppSessionStateOptions : SessionOptions
12+
public class RemoteAppSessionStateOptions
1413
{
1514
internal const string ApiKeyHeaderName = "X-SystemWebAdapter-RemoteAppSession-Key";
1615
internal const string ReadOnlyHeaderName = "X-SystemWebAdapter-RemoteAppSession-ReadOnly";

src/Microsoft.AspNetCore.SystemWebAdapters.SessionState/RemoteSession/RemoteSessionModule.Framework.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,24 @@
44
using System;
55
using System.Web;
66
using System.Web.SessionState;
7+
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
78

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

1011
internal sealed class RemoteSessionModule : IHttpModule
1112
{
1213
private readonly RemoteAppSessionStateOptions _options;
14+
private readonly ISessionSerializer _serializer;
1315

14-
public RemoteSessionModule(RemoteAppSessionStateOptions options)
16+
public RemoteSessionModule(RemoteAppSessionStateOptions options, ISessionSerializer serializer)
1517
{
1618
_options = options;
19+
_serializer = serializer;
1720
}
1821

1922
public void Init(HttpApplication context)
2023
{
21-
var handler = new RemoteAppSessionStateHandler(_options);
24+
var handler = new RemoteAppSessionStateHandler(_options, _serializer);
2225

2326
context.PostMapRequestHandler += MapRemoteSessionHandler;
2427

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ public interface ISessionSerializer
1414
Task DeserializeToAsync(Stream stream, HttpSessionState state, CancellationToken token);
1515

1616
Task SerializeAsync(HttpSessionState state, Stream stream, CancellationToken token);
17+
18+
byte[] Serialize(string key, object value);
19+
20+
object? Deserialize(string key, Memory<byte> bytes);
1721
}
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
5+
46
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
57

68
public interface ISessionSerializer
79
{
8-
ISessionState? Deserialize(string? data);
10+
ISessionState? Deserialize(string? input);
911

1012
byte[] Serialize(ISessionState state);
13+
14+
byte[] Serialize(string key, object value);
15+
16+
object? Deserialize(string key, Memory<byte> bytes);
1117
}

src/Microsoft.AspNetCore.SystemWebAdapters.SessionState/Serialization/SessionSerializer.Framework.cs renamed to src/Microsoft.AspNetCore.SystemWebAdapters.SessionState/Serialization/JsonSessionSerializer.Framework.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
1212

13-
internal partial class SessionSerializer
13+
internal partial class JsonSessionSerializer
1414
{
1515
public Task SerializeAsync(HttpSessionState state, Stream stream, CancellationToken token)
1616
{

0 commit comments

Comments
 (0)