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