Skip to content

#4867 Add Azure Table Storage ISessionStore package - #4957

Open
joshhanson314 wants to merge 4 commits into
MarimerLLC:mainfrom
joshhanson314:feature/4867-azure-table-storage-sessionstore
Open

joshhanson314 wants to merge 4 commits into
MarimerLLC:mainfrom
joshhanson314:feature/4867-azure-table-storage-sessionstore

Conversation

@joshhanson314

@joshhanson314 joshhanson314 commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Adds Csla.Blazor.State.AzureTableStorage, an opt-in NuGet package that implements ISessionStore (#4832) on Azure Table Storage. Discussed and agreed in #4867.

Usage

builder.Services.AddCsla(o => o
  .AddAspNetCore()
  .AddServerSideBlazor(b => b.UseInMemoryApplicationContextManager = false)
  .UseAzureTableStorageSessionStore(s => s.ConnectionString = "..."));

The store is only used when UseInMemoryApplicationContextManager is false. Call UseAzureTableStorageSessionStore after AddServerSideBlazor; it replaces the in-memory store registration.

The TableServiceClient comes from ConnectionString if set, otherwise from a keyed service (ServiceKey, for example one registered by Aspire), otherwise from the default TableServiceClient in the container. The package never registers a client itself, so it does not change which client the rest of the app receives.

Behavior

  • One row per session: PartitionKey is the session key, and RowKey is ApplicationDiscriminator, so several apps can share a table.
  • Live sessions are kept in memory and returned by reference, as InMemorySessionStore does, because SessionManager and ApplicationContextManagerBlazor change the returned session in place. The table is read only when a session is not in memory, such as after a restart.
  • Session data is written to the table by AddOrUpdateSessionAsync, for example when the WebAssembly client sends its state, under a per-key lock so the cache and the table agree. Changes made in place reach the table with the next such write.
  • PurgeSessions refreshes only the LastTouched column of live sessions, so other servers do not purge them. It never serializes them, because a change a request makes during serialization can produce data that cannot be deserialized. Apps should call it periodically.
  • Session data is split across up to 11 binary columns of 64 KiB (704 KiB), because Azure Tables limits a binary property to 64 KiB. A larger session fails with InvalidOperationException before anything is written or cached.
  • With more than one server, each user must stay on the same server, as Blazor Server already requires.
  • Read and write failures propagate, so a failed read can't be mistaken for a missing session.
  • Purge is best effort and deletes stale rows using the ETag from its query.
  • The table is created on first use if it does not exist.

Business objects restored from the table keep the ApplicationContext of the scope that deserialized them, as sessions that arrive through StateController.Put do today. Fixing that needs the caller's context, so it is left for a core change.

The in-memory cache could instead live in SessionManager, so every ISessionStore gets it. I can move it there if you prefer.

Tests

Csla.Blazor.State.AzureTableStorage.Tests (net10.0, net11.0):

  • 10 DI registration tests that need no storage, run in CI.
  • 15 tests against Azurite, marked SkipOnCIServer. Locally: docker run -p 10002:10002 mcr.microsoft.com/azure-storage/azurite azurite-table --tableHost 0.0.0.0.

All 25 pass locally on both target frameworks.

There is no backlog issue for this; the commit references the discussion.

🤖 Generated with Claude Code

Adds Csla.Blazor.State.AzureTableStorage, an opt-in ISessionStore for
Blazor state management backed by Azure Table Storage, enabled with
UseAzureTableStorageSessionStore() after AddServerSideBlazor().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Formatter lifetime, session write-back, timestamp preservation, and purge concurrency issues remain unresolved.

4 open findings
What changed in this PR

Adds an opt-in Azure Table Storage provider for CSLA’s pluggable ISessionStore, enabling persistent Blazor session storage.

Changes:

  • Implements session reads, upserts, application isolation, and expiration purging.
  • Adds configurable client resolution and dependency injection registration.
  • Adds registration tests, Azurite integration tests, and solution entries.
File Description
Source/​tests/​Csla.Blazor.State.AzureTableStorage.Tests/​Csla.Blazor.State.AzureTableStorage.Tests.csproj Configures the test project.
Source/​tests/​Csla.Blazor.State.AzureTableStorage.Tests/​ConfigurationExtensionsTests.cs Tests registration and client selection.
Source/​tests/​Csla.Blazor.State.AzureTableStorage.Tests/​AzureTableStorageSessionStoreTests.cs Tests persistence, isolation, and purging.
Source/​csla.test.sln Includes provider and tests.
Source/​csla.build.sln Includes the provider in framework builds.
Source/​Csla.Blazor.State.AzureTableStorage/​SessionEntity.cs Defines the stored session entity.
Source/​Csla.Blazor.State.AzureTableStorage/​Csla.Blazor.State.AzureTableStorage.csproj Configures the package and dependencies.
Source/​Csla.Blazor.State.AzureTableStorage/​AzureTableStorageSessionStore.cs Implements Azure-backed session operations.
Source/​Csla.Blazor.State.AzureTableStorage/​AzureTableStorageOptions.cs Defines storage configuration options.
Source/​Csla.Blazor.State.AzureTableStorage/​AzureTableStorageConfigurationExtensions.cs Registers the provider and resolves clients.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

…ssion store

Keep live sessions in memory and return them by reference, because
SessionManager and ApplicationContextManagerBlazor change the returned
session in place. The table is read on a cache miss, written on every
AddOrUpdateSessionAsync, and written again for live sessions during
purge. Purge deletes stale rows using the queried ETag.

Resolve the serialization formatter in a new scope per operation,
since it depends on the scoped ApplicationContext and keeps per-call
state.

Register with AddSingleton, register the options in DI, make the store
and SessionEntity internal, log through LoggerMessage methods, and use
an injected TimeProvider for the purge cutoff.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Restored-object lifetimes, concurrent persistence safety, and payload-size handling remain unresolved.

3 open findings
4 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Sessions exceeding Azure Table 64 KiB property limit cannot be saved

Source/​Csla.Blazor.State.AzureTableStorage/​SessionEntity.cs:48

Azure Table Storage limits a binary property to 64 KiB. Putting the entire serialized session in Value therefore makes sessions above that size fail on every write, even when they fit within the overall entity-size limit. Support larger payloads with bounded chunks and reassembly, or explicitly document and validate the serialized-size limit before publishing the session to the cache. Add boundary tests; if larger sessions are supported, include a round trip exceeding 64 KiB.

🧠 Review effort: Balanced

joshhanson314 and others added 2 commits October 23, 2026 23:00
… large sessions

Purge serialized sessions that requests change in place, which could
write data that cannot be deserialized, and could overwrite a newer
session saved for the same key. Purge now refreshes only the stored
LastTouched column of live sessions. AddOrUpdateSessionAsync serializes,
writes, and updates the cache under a per-key lock, publishing the
session only after the write succeeds.

Azure Tables limits a binary property to 64 KiB, so session data is
split across up to 11 columns (704 KiB). Larger sessions fail with
InvalidOperationException before anything is written or cached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rockfordlhotka

Copy link
Copy Markdown
Member

@StefanOssendorf are you happy with this PR?

@ossendorf-at-hoelscher

Copy link
Copy Markdown

@StefanOssendorf are you happy with this PR?

I'll review it this weekend. Sorry for taking so long. 😅

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants