Session State Migration
March 30, 2026 · View on GitHub
The SessionShim allows migrated Web Forms code to access Session["key"] without modification. Your existing session access code — shopping carts, user preferences, wizard state — works unchanged in Blazor.
Overview
What it does:
- Provides a
Sessionindexer (Session["key"]) backed by ASP.NET CoreISessionwith automatic JSON serialization - Falls back to an in-memory dictionary in interactive (SignalR) mode where HTTP session isn't available
- Supports type-safe access via
Session.Get<T>("key")for explicit deserialization - Registers automatically via
AddBlazorWebFormsComponents()
Why it matters:
Web Forms applications use Session["key"] everywhere — shopping carts, user preferences, wizard step tracking, temporary form data. Without a shim, every Session access would need to be rewritten to use Blazor's state management patterns. The SessionShim eliminates that refactoring during migration, letting you focus on UI conversion first.
Before and After
=== "Web Forms (Original)" ```csharp // ShoppingCart.aspx.cs protected void Page_Load(object sender, EventArgs e) { // Store cart ID in session if (Session["CartId"] == null) { Session["CartId"] = Guid.NewGuid().ToString(); }
var cartId = (string)Session["CartId"];
LoadCart(cartId);
}
protected void AddToCart_Click(object sender, EventArgs e)
{
// Track item count
int count = Session["ItemCount"] != null
? (int)Session["ItemCount"] : 0;
Session["ItemCount"] = count + 1;
}
protected void SetPreference_Click(object sender, EventArgs e)
{
// Store user preferences
Session["Theme"] = "dark";
Session["Language"] = "en-US";
}
```
=== "Blazor with BWFC (Same Code Works!)" ```csharp // ShoppingCart.razor.cs protected void Page_Load() { // Same session access — SessionShim handles it if (Session["CartId"] == null) { Session["CartId"] = Guid.NewGuid().ToString(); }
var cartId = (string)Session["CartId"];
LoadCart(cartId);
}
protected void AddToCart_Click()
{
// Same code — SessionShim serializes/deserializes automatically
int count = Session["ItemCount"] != null
? (int)Session["ItemCount"] : 0;
Session["ItemCount"] = count + 1;
}
protected void SetPreference_Click()
{
// Same code
Session["Theme"] = "dark";
Session["Language"] = "en-US";
}
```
Key difference: The Session indexer is provided by SessionShim instead of HttpContext.Session. Your code doesn't need to know the difference.
Setup
Automatic Registration
The SessionShim is registered automatically when you call AddBlazorWebFormsComponents():
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBlazorWebFormsComponents();
var app = builder.Build();
app.Run();
No additional setup needed for interactive (SignalR) mode — the shim uses in-memory storage automatically.
SSR Session Persistence
If your Blazor app uses Server-Side Rendering (SSR) and you need session data to persist across HTTP requests, add the session middleware:
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBlazorWebFormsComponents();
builder.Services.AddSession(); // Enable ASP.NET Core session
var app = builder.Build();
app.UseSession(); // Add session middleware to the pipeline
app.UseRouting();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.Run();
!!! note
app.UseSession() must be placed before app.UseRouting() in the middleware pipeline.
How It Works
The SessionShim operates in two modes depending on the Blazor hosting model:
SSR Mode (HTTP Context Available)
When an HttpContext with an active session is available, the shim wraps ASP.NET Core's ISession:
Session["CartId"] = "abc-123"
│
▼
SessionShim.SetItem("CartId", "abc-123")
│
▼
JSON.Serialize("abc-123") → ISession.SetString("CartId", json)
│
▼
Stored in ASP.NET Core distributed session (cookies, Redis, SQL, etc.)
Interactive Mode (SignalR Circuit)
When no HTTP session is available (interactive Blazor Server over SignalR), the shim falls back to an in-memory dictionary scoped to the current circuit:
Session["CartId"] = "abc-123"
│
▼
SessionShim.SetItem("CartId", "abc-123")
│
▼
Stored in ConcurrentDictionary<string, object> (per-circuit memory)
JSON Serialization
All values are serialized to JSON when stored and deserialized when retrieved. This means:
- Primitive types (
string,int,bool) work transparently - Complex objects must be JSON-serializable
- The cast syntax
(string)Session["key"]works because the shim deserializes back to the original type
Type-Safe Access
For explicit type control, use the generic Get<T>() method:
// Store a value
Session["ItemCount"] = 42;
// Retrieve with explicit type (recommended)
int count = Session.Get<int>("ItemCount");
// Retrieve with cast (also works)
int count = (int)Session["ItemCount"];
// Complex objects
Session["UserPrefs"] = new UserPreferences { Theme = "dark", Locale = "en-US" };
var prefs = Session.Get<UserPreferences>("UserPrefs");
!!! tip
Prefer Session.Get<T>() over casting when the stored type might be ambiguous. JSON deserialization of numeric types can return long instead of int, and Get<T>() handles the conversion correctly.
Limitations and Known Issues
| Feature | Behavior | Notes |
|---|---|---|
| Interactive mode storage | In-memory (per-circuit) | Data is lost when the circuit disconnects |
| Cross-tab sharing | ❌ Not supported in interactive mode | Each SignalR circuit has its own session dictionary |
| Session timeout | Follows ASP.NET Core session config (SSR) or circuit lifetime (interactive) | Configure via builder.Services.AddSession(options => ...) |
| Non-serializable objects | ❌ Throws on store | Objects must be JSON-serializable |
Session.Abandon() | Clears all keys | Does not destroy the underlying ASP.NET Core session in SSR mode |
Interactive Mode Caveats
In interactive Blazor Server mode (SignalR), session state is per-circuit:
- Opening the same page in two browser tabs creates two separate session stores
- Refreshing the page creates a new circuit, losing in-memory session data
- Session data does not survive server restarts
!!! warning
If your Web Forms app relied on session sharing across browser tabs or windows, you will need to migrate to a shared state solution (e.g., database-backed state, ProtectedBrowserStorage, or a distributed cache) in a later phase.
Troubleshooting
Session values are null after page refresh (Interactive mode)
This is expected in interactive mode — refreshing the page creates a new SignalR circuit with empty in-memory storage. To persist session data across refreshes:
- Enable SSR session persistence (see Setup above)
- Or migrate to
ProtectedBrowserStoragefor client-side persistence
"Object not serializable" exception
The SessionShim uses JSON serialization. Ensure your stored objects:
- Have parameterless constructors
- Have public properties (not fields)
- Don't contain circular references
Summary
The SessionShim:
- ✅ Lets existing
Session["key"]code work unchanged - ✅ Wraps ASP.NET Core
ISessionwith JSON serialization in SSR mode - ✅ Falls back to in-memory dictionary in interactive mode
- ✅ Supports type-safe access via
Session.Get<T>("key") - ✅ Registers automatically via
AddBlazorWebFormsComponents() - ❌ Interactive mode is per-circuit (not shared across tabs)
- ❌ In-memory storage is lost on circuit disconnect or page refresh
Use it for Phase 2 migrations when your code uses Session extensively. For long-term state management, consider migrating to Blazor-native patterns like ProtectedBrowserStorage, cascading parameters, or a state management service.
See ViewState and PostBack Shim for related state management patterns.