Skip to content

Commit

Permalink
Implement ISessionState against ISession (#21)
Browse files Browse the repository at this point in the history
  • Loading branch information
twsouthwick authored May 17, 2022
1 parent 96b0a9a commit 49ea3bd
Show file tree
Hide file tree
Showing 32 changed files with 955 additions and 65 deletions.
Binary file removed docs/session-state/readonly-remote-session.png
Binary file not shown.
47 changes: 36 additions & 11 deletions docs/session-state/remote-session.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,50 @@ In order to configure it, both the framework and core app must set an API key as

- `ApiKeyHeader` - header name that will contain an API key to secure the endpoint added on .NET Framework
- `ApiKey` - the shared API key that will be validated in the .NET Framework handler
- `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.

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

```csharp
builder.Services.AddSystemWebAdapters()
.AddJsonSessionSerializer(options =>
{
options.RegisterKey<int>("test-value");
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
})
.AddRemoteAppSession(options =>
{
options.RemoteApp = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);
options.ApiKey = "test-key";
options.RegisterKey<int>("test-value");
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
});
```

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

```csharp
Application.AddSystemWebAdapters()
.AddRemoteAppSession(options=>
{
options.ApiKey = "test-key";
options.RegisterKey<int>("test-value");
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
});
.AddRemoteAppSession(
options => options.ApiKey = "test-key",
options =>
{
options.RegisterKey<int>("test-value");
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
});
```
# Protocol

## Readonly
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.

![Readonly protocol](./readonly-remote-session.png)
```mermaid
sequenceDiagram
participant core as ASP.NET Core
participant framework as ASP.NET
participant session as Session Store
core ->> framework: GET /session
framework ->> session: Request session
session -->> framework: Session
framework -->> core: Session
```

## Writeable

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

![Writeable protocl](writeable-remote-session.png)
```mermaid
sequenceDiagram
participant core as ASP.NET Core
participant framework as ASP.NET
participant session as Session Store
core ->> framework: GET /session
framework ->> session: Request session
session -->> framework: Session
framework -->> core: Session
core ->> framework: PUT /session
framework ->> framework: Deserialize to HttpSessionState
framework -->> core: Session complete
framework ->> session: Persist
```
43 changes: 43 additions & 0 deletions docs/session-state/session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Session State

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:

- 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.
- 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.

The adapter infrastructure exposes two interfaces that can be used to implement any session storage system. These are:

- `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.
- `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.

## Serialization
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.

Serialization and deserialization of session keys requires additional information which is configured via the `SessionSerializerOptions`:

- `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.

To use the default JSON backed implementation, add the following to the startup:

```csharp
builder.Services.AddSystemWebAdapters()
.AddJsonSessionSerializer(options =>
{
options.RegisterKey<int>("test-value");
});
```

## Implementations

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.

- Strongly typed: Provides the ability to access an object and can be cast to the expected type
- Locking: Ensures multiple requests within a single session are queued up and aren't accessing the session at the same time
- Standalone: Can be used when there is just a .NET Core app without needing additional support.

Below are the available implementations:

| Implementation | Strongly typed | Locking | Standalone |
|-------------------------------------------------------------|----------------|---------|------------|
| [Remote app](remote-session.md) | ✔️ | ✔️ ||
| [Wrapped ASP.NET Core](wrapped-aspnetcore-session.md) | ✔️ || ✔️ |
17 changes: 17 additions & 0 deletions docs/session-state/wrapped-aspnetcore-session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Wrapped ASP.NET Core Session State

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.

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

```csharp
builder.Services.AddSystemWebAdapters()
.AddJsonSessionSerializer(options =>
{
options.RegisterKey<int>("test-value");
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
})
.WrapAspNetCoreSession();
```

The framework app would not need any changes to enable this behavior.
Binary file removed docs/session-state/writeable-remote-session.png
Binary file not shown.
4 changes: 2 additions & 2 deletions samples/ClassLibrary/SessionUtils.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using Microsoft.AspNetCore.SystemWebAdapters.SessionState;
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

namespace ClassLibrary;

public class SessionUtils
{
public static string ApiKey = "test-key";

public static void RegisterSessionKeys(SessionOptions options)
public static void RegisterSessionKeys(SessionSerializerOptions options)
{
options.RegisterKey<int>("test-value");
options.RegisterKey<SessionDemoModel>("SampleSessionItem");
Expand Down
8 changes: 3 additions & 5 deletions samples/MvcApp/Global.asax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ protected void Application_Start()

Application.AddSystemWebAdapters()
.AddProxySupport(options => options.UseForwardedHeaders = true)
.AddRemoteAppSession(options=>
{
options.ApiKey = ClassLibrary.SessionUtils.ApiKey;
ClassLibrary.SessionUtils.RegisterSessionKeys(options);
});
.AddRemoteAppSession(
options => options.ApiKey = ClassLibrary.SessionUtils.ApiKey,
options => ClassLibrary.SessionUtils.RegisterSessionKeys(options));
}
}
}
7 changes: 3 additions & 4 deletions samples/MvcCoreApp/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddSystemWebAdapters()
.AddJsonSessionSerializer(options => ClassLibrary.SessionUtils.RegisterSessionKeys(options))
.AddRemoteAppSession(options =>
{
options.RemoteApp = new(builder.Configuration["ReverseProxy:Clusters:fallbackCluster:Destinations:fallbackApp:Address"]);
options.ApiKey = ClassLibrary.SessionUtils.ApiKey;
ClassLibrary.SessionUtils.RegisterSessionKeys(options);
});

var app = builder.Build();
Expand All @@ -36,8 +35,8 @@
app.UseEndpoints(endpoints =>
{
app.MapDefaultControllerRoute();
// This method can be used to enable session (or read-only session) on all controllers
//.RequireSystemWebAdapterSession();
// This method can be used to enable session (or read-only session) on all controllers
//.RequireSystemWebAdapterSession();
app.MapReverseProxy();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6.0;netcoreapp3.1;net472</TargetFrameworks>
Expand Down Expand Up @@ -42,7 +42,7 @@
<Compile Include="RemoteSession/RemoteAppSessionStateOptions.cs" />
<Compile Include="Serialization/SessionValues.cs" />
<Compile Include="Serialization/SerializedSessionState.cs" />
<Compile Include="Serialization/SessionSerializer.Shared.cs" />
<Compile Include="Serialization/JsonSessionSerializer.Shared.cs" />

<Reference Include="System.Web" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@

using System;
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

namespace Microsoft.AspNetCore.SystemWebAdapters;

public static class RemoteAppSessionStateExtensions
{
public static ISystemWebAdapterBuilder AddRemoteAppSession(this ISystemWebAdapterBuilder builder, Action<RemoteAppSessionStateOptions> configure)
public static ISystemWebAdapterBuilder AddRemoteAppSession(this ISystemWebAdapterBuilder builder, Action<RemoteAppSessionStateOptions> configureRemote, Action<SessionSerializerOptions> configureSerializer)
{
var options = new RemoteAppSessionStateOptions();
configure(options);
builder.Modules.Add(new RemoteSessionModule(options));
configureRemote(options);

var serializerOptions = new SessionSerializerOptions();
configureSerializer(serializerOptions);
var serializer = new JsonSessionSerializer(serializerOptions.KnownKeys);

builder.Modules.Add(new RemoteSessionModule(options, serializer));
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using System.Net.Http;
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.SystemWebAdapters;
Expand All @@ -13,7 +12,6 @@ public static class RemoteAppSessionStateExtensions
{
public static ISystemWebAdapterBuilder AddRemoteAppSession(this ISystemWebAdapterBuilder builder, Action<RemoteAppSessionStateOptions> configure)
{
builder.Services.AddSingleton<ISessionSerializer, SessionSerializer>();
builder.Services.AddHttpClient<ISessionManager, RemoteAppSessionStateManager>()
// Disable cookies in the HTTP client because the service will manage the cookie header directly
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { UseCookies = false });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@ namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;
internal sealed class RemoteAppSessionStateHandler : HttpTaskAsyncHandler
{
private readonly RemoteAppSessionStateOptions _options;
private readonly SessionSerializer _serializer;
private readonly ISessionSerializer _serializer;

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

public override bool IsReusable => true;

public RemoteAppSessionStateHandler(RemoteAppSessionStateOptions options)
public RemoteAppSessionStateHandler(RemoteAppSessionStateOptions options, ISessionSerializer serializer)
{
if (string.IsNullOrEmpty(options.ApiKey))
{
throw new ArgumentOutOfRangeException("API key must not be empty.");
}

_options = options;
_serializer = new SessionSerializer(options.KnownKeys);
_serializer = serializer;
}

public override async Task ProcessRequestAsync(HttpContext context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
using System.ComponentModel.DataAnnotations;
#endif

using System.Collections.Generic;
using System;

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;

public class RemoteAppSessionStateOptions : SessionOptions
public class RemoteAppSessionStateOptions
{
internal const string ApiKeyHeaderName = "X-SystemWebAdapter-RemoteAppSession-Key";
internal const string ReadOnlyHeaderName = "X-SystemWebAdapter-RemoteAppSession-ReadOnly";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@
using System;
using System.Web;
using System.Web.SessionState;
using Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.RemoteSession;

internal sealed class RemoteSessionModule : IHttpModule
{
private readonly RemoteAppSessionStateOptions _options;
private readonly ISessionSerializer _serializer;

public RemoteSessionModule(RemoteAppSessionStateOptions options)
public RemoteSessionModule(RemoteAppSessionStateOptions options, ISessionSerializer serializer)
{
_options = options;
_serializer = serializer;
}

public void Init(HttpApplication context)
{
var handler = new RemoteAppSessionStateHandler(_options);
var handler = new RemoteAppSessionStateHandler(_options, _serializer);

context.PostMapRequestHandler += MapRemoteSessionHandler;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ public interface ISessionSerializer
Task DeserializeToAsync(Stream stream, HttpSessionState state, CancellationToken token);

Task SerializeAsync(HttpSessionState state, Stream stream, CancellationToken token);

byte[] Serialize(string key, object value);

object? Deserialize(string key, Memory<byte> bytes);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

public interface ISessionSerializer
{
ISessionState? Deserialize(string? data);
ISessionState? Deserialize(string? input);

byte[] Serialize(ISessionState state);

byte[] Serialize(string key, object value);

object? Deserialize(string key, Memory<byte> bytes);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

namespace Microsoft.AspNetCore.SystemWebAdapters.SessionState.Serialization;

internal partial class SessionSerializer
internal partial class JsonSessionSerializer
{
public Task SerializeAsync(HttpSessionState state, Stream stream, CancellationToken token)
{
Expand Down
Loading

0 comments on commit 49ea3bd

Please sign in to comment.