Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

History

283 Commits

Repository files navigation

ManagedCode.Storage logo

ManagedCode.Storage

build-and-testDocsReleaseCodeQLCodecovQuality Gate StatusCoverageMCAF.NETLicense: MITNuGet

Cross-provider blob storage toolkit for .NET and ASP.NET streaming scenarios.

Documentation

  • Published docs (GitHub Pages): https://storage.managed-code.com/
  • Source docs live in docs/:
    • Setup: docs/Development/setup.md
    • Credentials (OneDrive/Google Drive/Dropbox/CloudKit): docs/Development/credentials.md
    • Testing strategy: docs/Testing/strategy.md
    • Feature docs: docs/Features/index.md
    • ADRs: docs/ADR/index.md
    • API (HTTP + SignalR): docs/API/storage-server.md
  • Diagrams are Mermaid-based and are expected to render on GitHub and the docs site.

Table of Contents

Quickstart

1) Install a provider package

dotnet add package ManagedCode.Storage.FileSystem

2) Register as default IStorage

usingManagedCode.Storage.Core;usingManagedCode.Storage.FileSystem.Extensions;varbuilder=WebApplication.CreateBuilder(args);builder.Services.AddFileSystemStorageAsDefault(options =>{options.BaseFolder=Path.Combine(builder.Environment.ContentRootPath,"storage");});

3) Use IStorage

usingManagedCode.Storage.Core;publicsealedclassMyService(IStoragestorage){publicTaskUploadAsync(CancellationTokenct)=>storage.UploadAsync("hello", options =>options.FileName="hello.txt",ct);}

4) (Optional) Expose HTTP + SignalR endpoints

usingManagedCode.Storage.Server.Extensions.DependencyInjection;usingManagedCode.Storage.Server.Extensions;builder.Services.AddControllers();builder.Services.AddStorageServer();builder.Services.AddStorageSignalR();// optionalvarapp=builder.Build();app.MapControllers();// /api/storage/*app.MapStorageHub();// /hubs/storage

ManagedCode.Storage wraps vendor SDKs behind a single IStorage abstraction so uploads, downloads, metadata, streaming, and retention behave the same regardless of provider. Swap between Azure Blob Storage, Azure Data Lake, Amazon S3, Google Cloud Storage, OneDrive, Google Drive, Dropbox, CloudKit (iCloud app data), SFTP, browser storage with IndexedDB metadata and OPFS-backed payloads, and a local file system without rewriting application code — and optionally use the Virtual File System (VFS) overlay for a file/directory API on top of any configured IStorage. Pair it with our ASP.NET controllers, SignalR client, and Orleans grain persistence provider to deliver chunked uploads, ranged downloads, real-time progress, and actor-state persistence end to end.

Motivation

Cloud storage vendors expose distinct SDKs, option models, and authentication patterns. That makes it painful to change providers, run multi-region replication, or stand up hermetic tests. ManagedCode.Storage gives you a universal surface, consistent Result<T> handling, and DI-aware registration helpers so you can plug in any provider, test locally, and keep the same code paths in production.

Features

  • Unified IStorage abstraction covering upload, download, streaming, metadata, deletion, container management, and legal hold operations backed by Result<T> responses.
  • Provider coverage across Azure Blob Storage, Azure Data Lake, Amazon S3, Google Cloud Storage, OneDrive (Microsoft Graph), Google Drive, Dropbox, CloudKit (iCloud app data), SFTP, browser storage with IndexedDB metadata plus OPFS-backed payloads, and the local file system.
  • Keyed dependency-injection registrations plus default provider helpers to fan out files per tenant, region, or workload without manual service plumbing.
  • ManagedCode.Storage.Orleans lets Orleans grains persist IPersistentState<TState> through any registered ManagedCode IStorage, including typed and keyed DI setups.
  • ASP.NET storage controllers, chunk orchestration services, and a SignalR hub/client pair that deliver resumable uploads, ranged downloads, CRC32 validation, and real-time progress.
  • ManagedCode.Storage.Client brings streaming uploads/downloads, CRC32 helpers, and MIME discovery via MimeHelper to any .NET app.
  • Strongly typed option objects (UploadOptions, DownloadOptions, DeleteOptions, MetadataOptions, LegalHoldOptions, etc.) let you configure directories, metadata, and legal holds in one place.
  • Virtual File System package provides a file/directory API (IVirtualFileSystem) on top of the configured IStorage and can cache metadata for faster repeated operations, including browser storage verified through real Playwright flows in both Blazor WebAssembly and Interactive Server hosts.
  • Comprehensive automated test suite with cross-provider sync fixtures, multi-gigabyte streaming simulations (4 MB units per "GB"), ASP.NET controller harnesses, SFTP/local filesystem coverage, and Playwright browser verification for browser storage small-file overwrites, concurrent tabs, VFS flows, a fast 128 MiB browser large-file lane, and a separate 256 MiB browser stress lane in both Interactive Server and Blazor WebAssembly hosts.
  • ManagedCode.Storage.TestFakes package plus Testcontainers-based fixtures make it easy to run offline or CI tests without touching real cloud accounts.

Packages

Core & Utilities

PackageLatestDescription
ManagedCode.Storage.CoreNuGetCore abstractions, option models, CRC32/MIME helpers, and DI extensions.
ManagedCode.Storage.VirtualFileSystemNuGetVirtual file system overlay on top of IStorage (file/directory API + caching; not a provider).
ManagedCode.Storage.TestFakesNuGetProvider doubles for unit/integration tests without hitting cloud services.

Providers

PackageLatestDescription
ManagedCode.Storage.AzureNuGetAzure Blob Storage implementation with metadata, streaming, and legal hold support.
ManagedCode.Storage.Azure.DataLakeNuGetAzure Data Lake Gen2 provider on top of the unified abstraction.
ManagedCode.Storage.AwsNuGetAmazon S3 provider with Object Lock and legal hold operations.
ManagedCode.Storage.GcpNuGetGoogle Cloud Storage integration built on official SDKs.
ManagedCode.Storage.BrowserNuGetBrowser storage provider with Blazor DI helpers, MVC/static-asset helpers, IndexedDB metadata, and OPFS-backed payload streaming.
ManagedCode.Storage.FileSystemNuGetLocal file system implementation for hybrid or on-premises workloads.
ManagedCode.Storage.SftpNuGetSFTP provider powered by SSH.NET for regulated and air-gapped environments.
ManagedCode.Storage.OneDriveNuGetOneDrive provider built on Microsoft Graph.
ManagedCode.Storage.GoogleDriveNuGetGoogle Drive provider built on the Google Drive API.
ManagedCode.Storage.DropboxNuGetDropbox provider built on the Dropbox API.
ManagedCode.Storage.CloudKitNuGetCloudKit (iCloud app data) provider built on CloudKit Web Services.

Configuring OneDrive, Google Drive, Dropbox, and CloudKit

iCloud Drive does not expose a public server-side file API. ManagedCode.Storage.CloudKit targets CloudKit Web Services (iCloud app data), not iCloud Drive.

Credential guide: docs/Development/credentials.md.

These providers follow the same DI patterns as the other backends: use Add*StorageAsDefault(...) to bind IStorage, or Add*Storage(...) to inject the provider interface (IOneDriveStorage, IGoogleDriveStorage, IDropboxStorage, ICloudKitStorage).

Most cloud-drive providers expect you to create the official SDK client (Graph/Drive/Dropbox) with your preferred auth flow and pass it into the storage options. ManagedCode.Storage does not run OAuth flows automatically.

Keyed registrations are available as well (useful for multi-tenant apps):

usingManagedCode.Storage.Core;usingManagedCode.Storage.Dropbox.Extensions;builder.Services.AddDropboxStorageAsDefault("tenant-a", options =>{options.AccessToken=configuration["Dropbox:AccessToken"];// obtained via OAuth (see Dropbox section below)options.RootPath="/apps/my-app";});vartenantStorage=app.Services.GetRequiredKeyedService<IStorage>("tenant-a");

OneDrive / Microsoft Graph

  1. Install the provider package and import DI extensions:

    dotnet add package ManagedCode.Storage.OneDrive
    dotnet add package Azure.Identity
    usingManagedCode.Storage.OneDrive.Extensions;

    Docs: Register an app, Microsoft Graph auth.

  2. Create an app registration in Azure Active Directory (Entra ID) and record the Application (client) ID, Directory (tenant) ID, and a client secret.

  3. In API permissions, add Microsoft Graph permissions:

    • For server-to-server apps: ApplicationFiles.ReadWrite.All (or Sites.ReadWrite.All for SharePoint drives), then Grant admin consent.
    • For user flows: Delegated permissions are also possible, but you must supply a Graph client that authenticates as the user.
  4. Create the Graph client (example uses client credentials):

    usingAzure.Identity;usingMicrosoft.Graph;vartenantId=configuration["OneDrive:TenantId"]!;varclientId=configuration["OneDrive:ClientId"]!;varclientSecret=configuration["OneDrive:ClientSecret"]!;varcredential=newClientSecretCredential(tenantId,clientId,clientSecret);vargraphClient=newGraphServiceClient(credential,new[]{"https://graph.microsoft.com/.default"});
  5. Register OneDrive storage with the Graph client and the drive/root you want to scope to:

    builder.Services.AddOneDriveStorageAsDefault(options =>{options.GraphClient=graphClient;options.DriveId="me";// or a specific drive IDoptions.RootPath="app-data";// folder will be created when CreateContainerIfNotExists is trueoptions.CreateContainerIfNotExists=true;});
  6. If you need a concrete drive id, fetch it via Graph (example):

    vardrive=awaitgraphClient.Me.Drive.GetAsync();vardriveId=drive?.Id;

Google Drive

  1. Install the provider package and import DI extensions:

    dotnet add package ManagedCode.Storage.GoogleDrive
    usingManagedCode.Storage.GoogleDrive.Extensions;

    Docs: Drive API overview, OAuth 2.0.

  2. In Google Cloud Console, create a project and enable the Google Drive API.

  3. Create credentials:

    • Service account (recommended for server apps): create a service account and download a JSON key.
    • OAuth client (interactive user auth): configure OAuth consent screen and create an OAuth client id/secret.
  4. Create a DriveService.

    Service account example:

    usingGoogle.Apis.Auth.OAuth2;usingGoogle.Apis.Drive.v3;usingGoogle.Apis.Services;varcredential=GoogleCredential.FromFile("service-account.json").CreateScoped(DriveService.Scope.Drive);vardriveService=newDriveService(newBaseClientService.Initializer{HttpClientInitializer=credential,ApplicationName="MyApp"});

    If you use a service account, share the target folder/drive with the service account email (or use a Shared Drive) so it can see your files.

  5. Register the Google Drive provider with the configured DriveService and a root folder id:

    builder.Services.AddGoogleDriveStorageAsDefault(options =>{options.DriveService=driveService;options.RootFolderId="root";// or a specific folder id you control / shared team drive folder idoptions.CreateContainerIfNotExists=true;options.SupportsAllDrives=true;// To support shared/team drives});
  6. Store tokens in user secrets or environment variables; never commit them to source control.

Dropbox

  1. Install the provider package and import DI extensions:

    dotnet add package ManagedCode.Storage.Dropbox
    usingManagedCode.Storage.Dropbox.Extensions;

    Docs: Dropbox App Console, OAuth guide.

  2. Create an app in the Dropbox App Console and choose Scoped access with the Full Dropbox or App folder type.

  3. Record the App key and App secret (Settings tab).

  4. Under Permissions, enable files.content.write, files.content.read, files.metadata.read, and files.metadata.write (plus any additional scopes you need) and save changes.

  5. Obtain an access token:

    • For quick local testing, you can generate a token in the app console.
    • For production, use OAuth code flow (example):
    usingDropbox.Api;varappKey=configuration["Dropbox:AppKey"]!;varappSecret=configuration["Dropbox:AppSecret"]!;varredirectUri=configuration["Dropbox:RedirectUri"]!;// must be registered in Dropbox app console// 1) Redirect user to:// var authorizeUri = DropboxOAuth2Helper.GetAuthorizeUri(OAuthResponseType.Code, appKey, redirectUri, tokenAccessType: TokenAccessType.Offline);//// 2) Receive the 'code' on your redirect endpoint, then exchange it:varauth=awaitDropboxOAuth2Helper.ProcessCodeFlowAsync(code,appKey,appSecret,redirectUri);varaccessToken=auth.AccessToken;varrefreshToken=auth.RefreshToken;// store securely if you requested offline access
  6. Register Dropbox storage with a root path (use / for full access apps or /Apps/<your-app> for app folders). You can let the provider create the SDK client from credentials:

    builder.Services.AddDropboxStorageAsDefault(options =>{varaccessToken=configuration["Dropbox:AccessToken"]!;options.AccessToken=accessToken;options.RootPath="/apps/my-app";options.CreateContainerIfNotExists=true;});

    Or, for production, prefer refresh tokens (offline access):

    builder.Services.AddDropboxStorageAsDefault(options =>{options.RefreshToken=configuration["Dropbox:RefreshToken"]!;options.AppKey=configuration["Dropbox:AppKey"]!;options.AppSecret=configuration["Dropbox:AppSecret"];// optional when using PKCEoptions.RootPath="/apps/my-app";});
  7. Store tokens in user secrets or environment variables; never commit them to source control.

CloudKit (iCloud app data)

  1. Install the provider package and import DI extensions:

    dotnet add package ManagedCode.Storage.CloudKit
    usingManagedCode.Storage.CloudKit.Extensions;usingManagedCode.Storage.CloudKit.Options;

    Docs: CloudKit Web Services Reference.

  2. In Apple Developer / CloudKit Dashboard, configure the container you want to use and note its container id (example: iCloud.com.company.app).

    • ContainerId is an identifier (not a secret) and is typically derived from your App ID / bundle id.
  3. Ensure the file record type exists (default MCStorageFile).

  4. Add these fields to the record type:

    • path (String) — must be queryable/indexed for prefix listing.
    • contentType (String) — optional but recommended.
    • file (Asset) — stores the binary content.
  5. Configure authentication:

    • API token (ckAPIToken): create an API token for your container in CloudKit Dashboard and store it as a secret.
    • Server-to-server key (public DB only): create a CloudKit key in Apple Developer (download the .p8 private key, keep the key id).
  6. Register CloudKit storage:

    builder.Services.AddCloudKitStorageAsDefault(options =>{options.ContainerId="iCloud.com.company.app";// identifier, not a secretoptions.Environment=CloudKitEnvironment.Production;options.Database=CloudKitDatabase.Public;options.RootPath="app-data";// Choose ONE auth mode:options.ApiToken=configuration["CloudKit:ApiToken"];// OR:// options.ServerToServerKeyId = configuration["CloudKit:KeyId"];// options.ServerToServerPrivateKeyPem = configuration["CloudKit:PrivateKeyPem"]; // paste PEM (.p8) contents// Optional: provide a custom HttpClient (proxy, retries, test handler).// options.HttpClient = new HttpClient();});
  7. CloudKit Web Services impose size limits; keep files reasonably small and validate against your current CloudKit quotas.

Integrations

PackageLatestDescription
ManagedCode.Storage.OrleansNuGetOrleans grain persistence provider that stores grain state through any ManagedCode IStorage registration.
ManagedCode.Storage.ServerNuGetASP.NET controllers, chunk orchestration services, and the SignalR storage hub.
ManagedCode.Storage.ClientNuGet.NET client SDK for uploads, downloads, metadata, and SignalR negotiations.
ManagedCode.Storage.Client.SignalRNuGetSignalR streaming client for browsers and native applications.

Architecture

Storage Topology

The topology below shows how applications talk to the shared IStorage surface, optional Virtual File System, and keyed provider factories before landing on the concrete backends.

flowchart LR
subgraph Applications
API["ASP.NET Controllers"]
SignalRClient["SignalR Client"]
Workers["Background Services"]
end
subgraph Abstraction
Core["IStorage Abstractions"]
VFS["Virtual File System"]
Factories["Keyed Provider Factories"]
end
subgraph Providers
Azure["Azure Blob"]
AzureDL["Azure Data Lake"]
Aws["Amazon S3"]
Gcp["Google Cloud Storage"]
OneDrive["OneDrive (Graph)"]
GoogleDrive["Google Drive"]
Dropbox["Dropbox"]
CloudKit["CloudKit (iCloud app data)"]
Fs["File System"]
Sftp["SFTP"]
end
Applications --> Core
Core --> VFS
Core --> Factories
Factories --> Azure
Factories --> AzureDL
Factories --> Aws
Factories --> Gcp
Factories --> OneDrive
Factories --> GoogleDrive
Factories --> Dropbox
Factories --> CloudKit
Factories --> Fs
Factories --> Sftp
Loading

Keyed provider registrations let you resolve multiple named instances from dependency injection while reusing the same abstraction across Azure, AWS, Google Cloud Storage, Google Drive, OneDrive, Dropbox, CloudKit, SFTP, and local file system storage.

ASP.NET Streaming Controllers

Controllers in ManagedCode.Storage.Server expose minimal routes that stream directly between HTTP clients and blob providers. Uploads arrive as multipart forms or raw streams, flow through the unified IStorage abstraction, and land in whichever provider is registered. Downloads return FileStreamResult responses so browsers, SDKs, or background jobs can read blobs without buffering the whole payload in memory.

sequenceDiagram
participant Client as Client App
participant Controller as StorageController
participant Storage as IStorage
participant Provider as IStorage Provider
Client->>Controller: POST /storage/upload (stream)
Controller->>Storage: UploadAsync(stream, UploadOptions)
Storage->>Provider: Push stream to backend
Provider-->>Storage: Result<BlobMetadata>
Storage-->>Controller: Upload response
Controller-->>Client: 200 OK + metadata
Client->>Controller: GET /storage/download?file=video.mp4
Controller->>Storage: DownloadAsync(file)
Storage->>Provider: Open download stream
Provider-->>Storage: Result<Stream>
Storage-->>Controller: Stream payload
Controller-->>Client: Chunked response
Loading

Controllers remain thin: consumers can inherit and override actions to add custom routing, authorization, or telemetry while leaving the streaming plumbing intact.

Virtual File System (VFS)

Want a file/directory API on top of any configured IStorage (with optional metadata caching)? The ManagedCode.Storage.VirtualFileSystem package provides IVirtualFileSystem, which routes all operations through your registered storage provider.

usingManagedCode.Storage.FileSystem.Extensions;usingManagedCode.Storage.VirtualFileSystem.Core;usingManagedCode.Storage.VirtualFileSystem.Extensions;// 1) Register any IStorage provider (example: FileSystem)builder.Services.AddFileSystemStorageAsDefault(options =>{options.BaseFolder=Path.Combine(builder.Environment.ContentRootPath,"storage");});// 2) Add VFS overlaybuilder.Services.AddVirtualFileSystem(options =>{options.DefaultContainer="vfs";options.EnableCache=true;});// 3) Use IVirtualFileSystempublicsealedclassMyVfsService(IVirtualFileSystemvfs){publicasyncTaskWriteAsync(CancellationTokenct){varfile=awaitvfs.GetFileAsync("avatars/user-1.png",ct);awaitfile.WriteAllTextAsync("hello",cancellationToken:ct);}}

VFS is an overlay: it does not replace your provider. In tests, pair VFS with ManagedCode.Storage.TestFakes or the FileSystem provider pointed at a temp folder to avoid real cloud accounts.

Dependency Injection & Keyed Registrations

Every provider ships with default and provider-specific registrations, but you can also assign multiple named instances using .NET's keyed services. This makes it easy to route traffic to different containers/buckets (e.g. azure-primary vs. azure-dr) or to fan out a file to several backends:

usingAmazon;usingAmazon.S3;usingManagedCode.MimeTypes;usingMicrosoft.Extensions.DependencyInjection;usingSystem.IO;usingSystem.Threading;usingSystem.Threading.Tasks;builder.Services.AddAzureStorage("azure-primary", options =>{options.ConnectionString=configuration["Storage:Azure:Primary:ConnectionString"]!;options.Container="assets";}).AddAzureStorage("azure-dr", options =>{options.ConnectionString=configuration["Storage:Azure:Dr:ConnectionString"]!;options.Container="assets-dr";}).AddAWSStorage("aws-backup", options =>{options.PublicKey=configuration["Storage:Aws:AccessKey"]!;options.SecretKey=configuration["Storage:Aws:SecretKey"]!;options.Bucket="assets-backup";options.OriginalOptions=newAmazonS3Config{RegionEndpoint=RegionEndpoint.USEast1};});publicsealedclassAssetReplicator{privatereadonlyIAzureStorage_primary;privatereadonlyIAzureStorage_disasterRecovery;privatereadonlyIAWSStorage_backup;publicAssetReplicator([FromKeyedServices("azure-primary")]IAzureStorageprimary,[FromKeyedServices("azure-dr")]IAzureStoragesecondary,[FromKeyedServices("aws-backup")]IAWSStoragebackup){_primary=primary;_disasterRecovery=secondary;_backup=backup;}publicasyncTaskMirrorAsync(Streamcontent,stringfileName,CancellationTokencancellationToken=default){varuploadOptions=newUploadOptions(fileName,mimeType:MimeHelper.GetMimeType(fileName));if(content.CanSeek){content.Position=0;await_primary.UploadAsync(content,uploadOptions,cancellationToken);content.Position=0;await_disasterRecovery.UploadAsync(content,uploadOptions,cancellationToken);content.Position=0;await_backup.UploadAsync(content,uploadOptions,cancellationToken);return;}awaitusingvarbufferFile=LocalFile.FromRandomNameWithExtension(fileName);awaitbufferFile.CopyFromStreamAsync(content,cancellationToken);await_primary.UploadAsync(bufferFile.FileInfo,uploadOptions,cancellationToken);await_disasterRecovery.UploadAsync(bufferFile.FileInfo,uploadOptions,cancellationToken);await_backup.UploadAsync(bufferFile.FileInfo,uploadOptions,cancellationToken);}}

Keyed services can also be resolved via IServiceProvider.GetRequiredKeyedService<T>("key") when manual dispatching is required.

Want to double-check data fidelity after copying? Pair uploads with Crc32Helper:

vardownload=await_backup.DownloadAsync(fileName,cancellationToken);download.IsSuccess.ShouldBeTrue();awaitusingvarlocal=download.Value;varcrc=Crc32Helper.CalculateFileCrc(local.FilePath);logger.LogInformation("Backup CRC for {File} is {Crc}",fileName,crc);

The test suite includes end-to-end scenarios that mirror payloads between Azure, AWS, the local file system, and virtual file systems; multi-gigabyte flows execute by default across every provider using 4 MB units per "GB" to keep runs fast while still exercising streaming paths.

Orleans Grain Persistence

ManagedCode.Storage.Orleans plugs Orleans IGrainStorage into the same provider-agnostic IStorage surface as the rest of the repository. That means a grain can persist through FileSystem today, then move to Azure Blob Storage, S3, or another backend later without rewriting grain code.

Typed DI registration resolves a specific storage service from the container:

usingManagedCode.Storage.FileSystem;usingManagedCode.Storage.FileSystem.Extensions;usingOrleans.Hosting;usingOrleans.Runtime;builder.Services.AddFileSystemStorageAsDefault(options =>{options.BaseFolder=Path.Combine(builder.Environment.ContentRootPath,"grain-state");});builder.UseOrleans(siloBuilder =>{siloBuilder.AddGrainStorage<IFileSystemStorage>("profiles", options =>{options.StateDirectory="orleans";options.DeleteStateOnClear=true;});});publicsealedclassProfileGrain([PersistentState("profile","profiles")]IPersistentState<ProfileState>profile):Grain,IProfileGrain{publicasyncTaskSetNameAsync(stringname){profile.State.Name=name;awaitprofile.WriteStateAsync();}}

If you already fan out storages via keyed DI, point Orleans at the keyed instance instead:

usingManagedCode.Storage.FileSystem.Extensions;usingOrleans.Hosting;builder.Services.AddFileSystemStorageAsDefault("tenant-a", options =>{options.BaseFolder=Path.Combine(builder.Environment.ContentRootPath,"tenant-a-state");});builder.UseOrleans(siloBuilder =>{siloBuilder.AddGrainStorage("profiles","tenant-a", options =>{options.StateDirectory="orleans";options.PathBuilder= context =>$"state/{context.ProviderName}/{context.StateName}/{context.GrainId}.state";});});

Registration resolves backing storage in this order: StorageFactory, StorageServiceType + StorageKey, StorageServiceType, StorageKey, then default IStorage. The provider also stores a logical Orleans ETag alongside the serialized state and throws InconsistentStateException when a stale write is detected during its read-before-write check.

ASP.NET Controllers & Streaming

The ManagedCode.Storage.Server package surfaces upload/download controllers that pipe HTTP streams straight into the storage abstraction. Files can be sent as multipart forms or raw streams, while downloads return FileStreamResult so large assets flow back to the caller without buffering in memory.

// Program.cs / Startup.csbuilder.Services.AddStorageServer(options =>{options.EnableRangeProcessing=true;// support range/seek operationsoptions.InMemoryUploadThresholdBytes=512*1024;// spill to disk after 512 KBoptions.InMemoryDownloadThresholdBytes=512*1024;// guard APIs that materialize bytes in memory});app.MapControllers();// exposes /api/storage/* endpoints by default

When you need custom routes, validation, or policies, inherit from the base controller and reuse the same streaming helpers:

[Route("api/files")]publicsealedclassFilesController:StorageControllerBase<IMyCustomStorage>{publicFilesController(IMyCustomStoragestorage,ChunkUploadServicechunks,StorageServerOptionsoptions):base(storage,chunks,options){}}// Upload a form file directly into storagepublicTask<IActionResult>Upload(IFormFilefile,CancellationTokenct)=>UploadFormFileAsync(file,ct);// Stream a blob to the client in real timepublicTask<IActionResult>Download(stringfileName,CancellationTokenct)=>DownloadAsStreamAsync(fileName,ct);

Need resumable uploads or live progress UI? Call AddStorageSignalR() to enable the optional hub and connect with the ManagedCode.Storage.Client.SignalR package; otherwise, the controllers alone cover straight HTTP streaming scenarios.

Connection modes

Each provider supports two DI patterns:

  • Default mode: register a provider as the app-wide IStorage (you have one default storage).
  • Provider-specific mode: register the provider interface (IAzureStorage, IAWSStorage, etc.) and/or multiple storages via keyed services.

Cloud-drive providers (OneDrive, Google Drive, Dropbox) and CloudKit are configured in Configuring OneDrive, Google Drive, Dropbox, and CloudKit; the same default/provider-specific rules apply.

The Blazor browser-local provider follows the same Add*StorageAsDefault(...) and Add*Storage(...) pattern, but it is Scoped because it relies on Blazor IJSRuntime.

Azure

Default mode connection:

// Startup.csservices.AddAzureStorageAsDefault(newAzureStorageOptions{Container="{YOUR_CONTAINER_NAME}",ConnectionString="{YOUR_CONNECTION_STRING}",});

Using in default mode:

// MyService.cspublicclassMyService{privatereadonlyIStorage_storage;publicMyService(IStoragestorage){_storage=storage;}}

Provider-specific mode connection:

// Startup.csservices.AddAzureStorage(newAzureStorageOptions{Container="{YOUR_CONTAINER_NAME}",ConnectionString="{YOUR_CONNECTION_STRING}",});

Using in provider-specific mode

// MyService.cspublicclassMyService{privatereadonlyIAzureStorage_azureStorage;publicMyService(IAzureStorageazureStorage){_azureStorage=azureStorage;}}

Need multiple Azure accounts or containers? Call services.AddAzureStorage("azure-primary", ...) and decorate constructor parameters with [FromKeyedServices("azure-primary")].

Google Cloud (Click here to expand)

Google Cloud

Default mode connection:

// Startup.csservices.AddGCPStorageAsDefault(opt =>{opt.GoogleCredential=GoogleCredential.FromFile("{PATH_TO_YOUR_CREDENTIALS_FILE}.json");opt.BucketOptions=newBucketOptions(){ProjectId="{YOUR_API_PROJECT_ID}",Bucket="{YOUR_BUCKET_NAME}",};});

Using in default mode:

// MyService.cspublicclassMyService{privatereadonlyIStorage_storage;publicMyService(IStoragestorage){_storage=storage;}}

Provider-specific mode connection:

// Startup.csservices.AddGCPStorage(newGCPStorageOptions{BucketOptions=newBucketOptions(){ProjectId="{YOUR_API_PROJECT_ID}",Bucket="{YOUR_BUCKET_NAME}",}});

Using in provider-specific mode

// MyService.cspublicclassMyService{privatereadonlyIGCPStorage_gcpStorage;publicMyService(IGCPStoragegcpStorage){_gcpStorage=gcpStorage;}}

Need parallel GCS buckets? Register them with AddGCPStorage("gcp-secondary", ...) and inject via [FromKeyedServices("gcp-secondary")].

Amazon (Click here to expand)

Amazon

Default mode connection:

// Startup.cs// Tip for LocalStack: configure the client and set ServiceURL to the emulator endpoint.varawsConfig=newAmazonS3Config{RegionEndpoint=RegionEndpoint.EUWest1,ForcePathStyle=true,UseHttp=true,ServiceURL="http://localhost:4566"// LocalStack default endpoint};services.AddAWSStorageAsDefault(opt =>{opt.PublicKey="{YOUR_PUBLIC_KEY}";opt.SecretKey="{YOUR_SECRET_KEY}";opt.Bucket="{YOUR_BUCKET_NAME}";opt.OriginalOptions=awsConfig;});

Using in default mode:

// MyService.cspublicclassMyService{privatereadonlyIStorage_storage;publicMyService(IStoragestorage){_storage=storage;}}

Provider-specific mode connection:

// Startup.csservices.AddAWSStorage(newAWSStorageOptions{PublicKey="{YOUR_PUBLIC_KEY}",SecretKey="{YOUR_SECRET_KEY}",Bucket="{YOUR_BUCKET_NAME}",OriginalOptions=awsConfig});

Using in provider-specific mode

// MyService.cspublicclassMyService{privatereadonlyIAWSStorage_storage;publicMyService(IAWSStoragestorage){_storage=storage;}}

Need parallel S3 buckets? Register them with AddAWSStorage("aws-backup", ...) and inject via [FromKeyedServices("aws-backup")].

Browser Storage (Click here to expand)

Browser Storage

Default mode connection:

// Program.csbuilder.Services.AddBrowserStorageAsDefault(options =>{options.ContainerName="drafts";options.DatabaseName="managedcode-storage";options.ChunkSizeBytes=4*1024*1024;options.ChunkBatchSize=4;});

Recommended tuning for large browser-local files:

  • ChunkSizeBytes = 4 * 1024 * 1024 keeps browser read windows reasonable without forcing giant single-message allocations.
  • ChunkBatchSize = 4 lets uploads and reads move 4 contiguous chunks per JS interop window instead of one-by-one.
  • In Blazor Server or Interactive Server, keep HubOptions.MaximumReceiveMessageSize >= 32 * 1024 * 1024 when using the 4 MiB x 4 read window.
  • For the heaviest browser-local media workflows, prefer Blazor WebAssembly because it avoids the SignalR hop on reads.

Using in default mode:

// DraftService.cspublicsealedclassDraftService{privatereadonlyIStorage_storage;publicDraftService(IStoragestorage){_storage=storage;}}

Provider-specific mode connection:

// Program.csbuilder.Services.AddBrowserStorage(options =>{options.ContainerName="drafts";options.DatabaseName="managedcode-storage";options.ChunkSizeBytes=4*1024*1024;options.ChunkBatchSize=4;});

Using in provider-specific mode:

// DraftService.cspublicsealedclassDraftService{privatereadonlyIBrowserStorage_storage;publicDraftService(IBrowserStoragestorage){_storage=storage;}}

If an MVC or Razor Pages application needs the packaged browser module path for custom JavaScript integration, use ManagedCode.Storage.Browser.Mvc.BrowserStorageStaticAssetPaths.ModulePath.

Browser caveats: use this provider only after the app becomes interactive. In Blazor Server, don't call it during prerendering and keep HubOptions.MaximumReceiveMessageSize aligned with your effective read window, not just a single chunk. With ChunkSizeBytes = 4 MiB and ChunkBatchSize = 4, keep the receive limit at 32 MiB or higher. Prefer WebAssembly for the heaviest client-local media workflows. Treat browser storage as user-visible and user-modifiable.

Browser payloads use the browser Origin Private File System (OPFS). IndexedDB stays in the design only for blob metadata and list or lookup operations. If OPFS is unavailable in the current browser, uploads fail fast instead of silently falling back to a second payload backend.

The real Playwright browser hosts in this repo verify small-file saves and overwrites, concurrent tabs, VFS flows, a default 128 MiB large-file path, and a separate 256 MiB browser stress lane in both Blazor WebAssembly and Interactive Server. The large-file flows emit progress logs every 100 MiB, and both the small-file and large-file paths assert that payload storage resolves to OPFS.

FileSystem (Click here to expand)

FileSystem

Default mode connection:

// Startup.csservices.AddFileSystemStorageAsDefault(opt =>{opt.BaseFolder=Path.Combine(Environment.CurrentDirectory,"{YOUR_BUCKET_NAME}");});

Using in default mode:

// MyService.cspublicclassMyService{privatereadonlyIStorage_storage;publicMyService(IStoragestorage){_storage=storage;}}

Provider-specific mode connection:

// Startup.csservices.AddFileSystemStorage(newFileSystemStorageOptions{BaseFolder=Path.Combine(Environment.CurrentDirectory,"{YOUR_BUCKET_NAME}"),});

Using in provider-specific mode

// MyService.cspublicclassMyService{privatereadonlyIFileSystemStorage_fileSystemStorage;publicMyService(IFileSystemStoragefileSystemStorage){_fileSystemStorage=fileSystemStorage;}}

Mirror to multiple folders? Use AddFileSystemStorage("archive", options => options.BaseFolder = ...) and resolve them via [FromKeyedServices("archive")].

How to use

We assume that below code snippets are placed in your service class with injected IStorage:

publicclassMyService{privatereadonlyIStorage_storage;publicMyService(IStoragestorage){_storage=storage;}}

Upload

await_storage.UploadAsync(newStream());await_storage.UploadAsync("some string content");await_storage.UploadAsync(newFileInfo("D:\\my_report.txt"));

Delete

await_storage.DeleteAsync("my_report.txt");

Download

varlocalFile=await_storage.DownloadAsync("my_report.txt");

Get metadata

await_storage.GetBlobMetadataAsync("my_report.txt");

Native client

If you need more flexibility, you can use native client for any IStorage<T>

_storage.StorageClient

Conclusion

In summary, Storage library provides a universal interface for accessing and manipulating data in different cloud blob storage providers, plus ready-to-host ASP.NET controllers, SignalR streaming endpoints, keyed dependency injection, and a memory-backed VFS. It makes it easy to switch between providers or to use multiple providers simultaneously, without having to learn and use multiple APIs, while staying in full control of routing, thresholds, and mirroring. We hope you find it useful in your own projects!

About

Storage library provides a universal interface for accessing and manipulating data in different cloud blob storage providers

Topics

Resources

Stars

136 stars

Watchers

4 watching

Forks

Releases

Used by

Contributors

Languages