Feature: Browser Storage Provider (ManagedCode.Storage.Browser)
Purpose
Implement IStorage on top of browser storage primitives so browser-facing .NET applications can persist client-local payloads behind the same storage abstraction used by the rest of the repository:
- per-browser persistence across reloads and browser restarts
IndexedDBmetadata plus OPFS-backed payloads- DI-friendly storage access inside Blazor components and scoped services
- one shared browser script contract that can also be referenced from ASP.NET MVC or Razor Pages
This provider targets browser storage, not protected storage. Data remains user-visible and user-modifiable, and browser APIs are unavailable during prerendering.
Main Flows
flowchart LR
App --> Storage["BrowserStorage : IBrowserStorage"]
Storage --> JS["IJSRuntime + JS module"]
JS --> Browser["IndexedDB metadata + OPFS payload files"]
Components
Storages/ManagedCode.Storage.Browser/BrowserStorage.csStorages/ManagedCode.Storage.Browser/BrowserStorageProvider.cs- DI:
- Options:
- JS module:
- MVC or Razor asset helper:
DI Wiring
dotnet add package ManagedCode.Storage.Browser
using ManagedCode.Storage.Browser.Extensions;
builder.Services.AddBrowserStorageAsDefault(options =>
{
options.ContainerName = "drafts";
options.DatabaseName = "managedcode-storage";
options.ChunkSizeBytes = 4 * 1024 * 1024;
options.ChunkBatchSize = 4;
});
Inject the typed provider or default IStorage in a Blazor-scoped service or component after the app becomes interactive:
public sealed class DraftService(IBrowserStorage storage)
{
public Task<Result<BlobMetadata>> SaveAsync(Stream content, CancellationToken cancellationToken)
{
return storage.UploadAsync(content, new UploadOptions
{
FileName = "draft.json",
MimeType = "application/json"
}, cancellationToken);
}
}
Recommended tuning for larger browser-local payloads:
ChunkSizeBytes = 4 * 1024 * 1024ChunkBatchSize = 4- Blazor Server or Interactive Server:
HubOptions.MaximumReceiveMessageSize >= 32L * 1024 * 1024
If the same application also uses Interactive Server rendering, keep the SignalR receive limit above the full browser read window because browser-to-server JS interop responses are capped by HubOptions.MaximumReceiveMessageSize:
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddHubOptions(options =>
{
options.MaximumReceiveMessageSize = 32L * 1024 * 1024;
});
Current Behavior
- Uses a package-owned JS module and
IJSRuntime; consumers don’t need to author a helper script. - Registers
IBrowserStorageand defaultIStorageasScopedbecause browser storage depends on scopedIJSRuntime. - Maps directories and file names into a namespaced key space inside browser
IndexedDB. - Stores blob metadata separately from payload bytes so list and existence operations don’t have to materialize file contents.
- Stores payload bytes in OPFS and keeps
IndexedDBfocused on metadata only. - Requires OPFS for payload writes and fails fast when the browser does not expose the required OPFS APIs.
- Uploads streams chunk-by-chunk, can batch multiple contiguous chunks into one JS interop window, and exposes a lazy OPFS-backed
Stream. - A canceled upload discards its staged OPFS payload. Overwrites use one IndexedDB transaction to replace metadata and identify the superseded payload for cleanup, including writes to the same key from different tabs.
- Exposes a stable static asset path so MVC or Razor Pages apps can reference the same browser script contract.
- Supports
ManagedCode.Storage.VirtualFileSystemon top of the same provider without a browser-specific VFS fork; the real browser hosts exercise small-file save and overwrite flows, read, list, move, delete, large-payload roundtrips, and multi-tab flows end to end. - The browser host playgrounds emit progress logs every
100 MiBfor the1 GiBsave and load verification flows.
Caveats
- Browser storage isn’t available during prerendering. In Blazor Server, wait until
OnAfterRenderAsync(firstRender: true)or disable prerendering for the affected subtree. IndexedDBis shared across tabs for the same origin.- OPFS is origin-scoped and is cleared with site data; this provider now depends on OPFS for payload storage.
- Data is readable and mutable by the user; don’t store secrets or sensitive data there.
- Blazor WebAssembly is the primary runtime for heavier browser-local media flows because it avoids the server-side SignalR hop.
- In Interactive Server mode, browser-to-server JS interop responses are limited by
HubOptions.MaximumReceiveMessageSize; keep it aligned withChunkSizeBytes * ChunkBatchSizeplus headroom for interop framing. - Browser quotas still apply. Practical limits depend on the origin quota the browser grants to IndexedDB metadata and OPFS files.
Tests
Tests/ManagedCode.Storage.Tests/Storages/Browser/BrowserServerStorageIntegrationTests.csTests/ManagedCode.Storage.Tests/Storages/Browser/BrowserWasmStorageIntegrationTests.csTests/ManagedCode.Storage.Tests/Storages/Browser/BrowserServerVfsIntegrationTests.csTests/ManagedCode.Storage.Tests/Storages/Browser/BrowserWasmVfsIntegrationTests.csTests/ManagedCode.Storage.Tests/Storages/Browser/BrowserStoragePage.csTests/ManagedCode.Storage.Tests/Storages/Browser/BrowserServerHostFixture.csTests/ManagedCode.Storage.Tests/Storages/Browser/BrowserWasmHostFixture.csTests/ManagedCode.Storage.BrowserServerHost/Tests/ManagedCode.Storage.BrowserWasmHost/