Getting started
Resourcefull needs .NET 10 or later.
Install
Install the client package and one storage provider:
dotnet add package Resourcefull
dotnet add package Resourcefull.InMemory # in process memory: tests and trying things out
dotnet add package Resourcefull.SqlServer # SQL Server: production
dotnet add package Resourcefull.PostgreSql # PostgreSQL: production
dotnet add package Resourcefull.Sqlite # a SQLite database file: one server, desktop and edge
dotnet add package Resourcefull.FileSystem # a directory on a disk or a file share: production
dotnet add package Resourcefull.S3 # Amazon S3 or another S3-compatible store: production
dotnet add package Resourcefull.AzureBlob # Azure Blob Storage: production
Register Resourcefull
AddResourcefull registers IResourceServiceClient with your application's services. Choose exactly one provider
inside it:
using Resourcefull;
using Resourcefull.InMemory;
builder.Services.AddResourcefull(options => options.UseInMemory());
For SQL Server, pass the connection string of the database that will hold Resourcefull's tables, and provision that database. Here it happens at startup, which suits a single instance:
using Resourcefull;
using Resourcefull.SqlServer;
builder.Services.AddResourcefull(options => options
.UseSqlServer(builder.Configuration.GetConnectionString("Resources")!)
.UseVersioning(maxVersions: 10));
var app = builder.Build();
await app.Services.MigrateResourcefullDatabaseAsync();
With several instances, provision from a deployment step instead: on SQL Server, the first provisioning turns on a
database setting that briefly disconnects every other session. SQL Server covers both. For
PostgreSQL, call UsePostgreSql in place of UseSqlServer; see PostgreSQL. For SQLite, call
UseSqlite with the database file's connection string; see SQLite. To keep the
files in a directory, call UseFileSystem with its path, and skip provisioning; see
File system. For an S3-compatible store, call UseS3 with the bucket's name, and skip
provisioning; see S3-compatible storage. For Azure Blob Storage, call UseAzureBlobStorage with the
blob container's name and a client, and skip provisioning; see Azure Blob Storage.
UseVersioning is optional; see Versioning, and give every application that shares the database,
directory, bucket or blob container the same setting.
Settings from configuration
UseConfiguration reads Resourcefull's settings from a section of your configuration, such as appsettings.json, so
they can differ between environments without a rebuild:
{
"Resourcefull": {
"Versioning": { "MaxVersions": 10 },
"Expiry": { "Interval": "00:01:00" },
"Telemetry": { "RecordResourceNames": false },
"ContainerPolicies": { "Defaults": { "MaxResourceSize": "100 MB" } }
}
}
builder.Services.AddResourcefull(options => options
.UseSqlServer(builder.Configuration.GetConnectionString("Resources")!)
.UseConfiguration(builder.Configuration.GetSection("Resourcefull")));
- Each section turns its feature on:
VersioningasUseVersioningdoes,ExpiryasUseExpiry,TelemetryasUseTelemetry, andContainerPoliciesasUseContainerPolicies. An empty section, such as"Versioning": {}, turns it on with its defaults. Leave a section out to leave the feature off. - Times are written as hours, minutes and seconds, such as
00:01:00for a minute, and sizes as text, such as100 MB. - A key that isn't a setting, or a value it can't use, stops the application at startup with an error naming it:
a misspelled key such as
MaxVersion,"Versioning": falsewhere a section belongs, or"MaxVersions": 0. To leave a feature off, leave its section out. - Code wins when it comes after:
.UseConfiguration(section).UseVersioning(maxVersions: 3)keeps 3 versions whatever the section says. - The storage provider, the policy provider, the cleanup handler and the upload inspector are set in code, since each is a type of yours rather than a setting.
Store and read a file
Inject IResourceServiceClient. It manages containers; a container client works with the resources in one
container.
public class Invoices(IResourceServiceClient resources)
{
public async Task<ResourceInfo> SaveAsync(string fileName, Stream content, CancellationToken cancellationToken)
{
IResourceContainerClient invoices =
await resources.CreateContainerIfNotExistsAsync("invoices", cancellationToken: cancellationToken);
return await invoices.UploadAsync(fileName, content, new ResourceUploadOptions
{
Mode = ResourceWriteMode.Replace,
Headers = new ResourceHeaders("application/pdf", null)
}, cancellationToken);
}
public async Task<ResourceDownloadResult?> OpenAsync(string fileName, CancellationToken cancellationToken)
=> await resources.GetContainerClient("invoices").DownloadStreamingAsync(fileName, cancellationToken: cancellationToken);
}
UploadAsyncreads the stream from its current position to its end and returns what it stored, including the measuredContentLengthand a SHA-256ContentHash.DownloadStreamingAsyncreturnsnullwhen there's no such resource. Otherwise itsStreamreads the content from storage as you read it, so memory use stays flat whatever the file size. Dispose the stream when you're done.GetContainerClientdoesn't contact storage, so it's cheap to call per request. A container client is a lightweight handle: there's nothing to dispose.
List, inspect and delete with the same client:
IResourceContainerClient invoices = resources.GetContainerClient("invoices");
await foreach (ResourceInfo invoice in invoices.ListResourcesAsync()) Console.WriteLine(invoice.Name);
ResourceInfo? info = await invoices.GetResourceInfoAsync("2026-10.pdf");
bool deleted = await invoices.DeleteResourceIfExistsAsync("2026-10.pdf");
Your first file API
Here's a complete ASP.NET Core app that stores files you send it and gives them back, using the
Resourcefull.AspNetCore package for the HTTP side:
dotnet new web -n Files
cd Files
dotnet add package Resourcefull.SqlServer
dotnet add package Resourcefull.AspNetCore
Point it at a database in appsettings.json. On Windows, LocalDB comes with Visual Studio; anywhere else, run SQL
Server in a container (docker run -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD=Your_password1 -p 1433:1433 mcr.microsoft.com/mssql/server:2022-latest) and use Server=localhost;User Id=sa;Password=Your_password1; TrustServerCertificate=True;Database=Files instead:
{
"ConnectionStrings": {
"Resources": "Server=(localdb)\MSSQLLocalDB;Database=Files;Trusted_Connection=True"
}
}
Then replace Program.cs:
using Resourcefull;
using Resourcefull.AspNetCore;
using Resourcefull.SqlServer;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddResourcefull(options => options
.UseSqlServer(builder.Configuration.GetConnectionString("Resources")!)
.UseVersioning(maxVersions: 10));
// Missing files answer 404, a broken rule 400 or 409, rather than 500.
builder.Services.AddResourcefullExceptionHandler();
var app = builder.Build();
app.UseExceptionHandler();
await app.Services.MigrateResourcefullDatabaseAsync();
// The container the endpoints store into, created once.
using (IServiceScope scope = app.Services.CreateScope())
await scope.ServiceProvider.GetRequiredService<IResourceServiceClient>().CreateContainerIfNotExistsAsync("files");
app.MapPut("/files/{**name}", (string name, HttpRequest request, IResourceServiceClient resources,
CancellationToken cancellationToken) =>
resources.GetContainerClient("files").UploadFromRequestAsync(name, request, null, cancellationToken));
app.MapGet("/files/{**name}", (string name, IResourceServiceClient resources) =>
Results.Resource(resources.GetContainerClient("files"), name));
app.Run();
The download endpoint's Results.Resource(container, name), which also answers range requests, is new in 4.0
4.0. With 3.1, download first and pass the result, as Serving downloads
shows.
Run it with dotnet run, then send it a file and read it back, using the address dotnet run prints:
curl -X PUT -H "Content-Type: application/pdf" --data-binary @report.pdf http://localhost:5000/files/2026/report.pdf
curl -o copy.pdf http://localhost:5000/files/2026/report.pdf
The first curl answers 201 Created with what was stored, the second downloads it. Uploading again keeps the
older version, because of UseVersioning.
This app lets anyone read and write every file, which is fine on your own machine. Before it goes anywhere else, read Securing file endpoints and Going to production.
In a console app or worker
Resourcefull registers its clients as scoped services, so outside a web request create a scope for each unit of work:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Resourcefull;
using Resourcefull.SqlServer;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddResourcefull(options => options
.UseSqlServer(builder.Configuration.GetConnectionString("Resources")!)
.UseVersioning(maxVersions: 10)); // the same as every other application using this database
using IHost host = builder.Build();
await host.Services.MigrateResourcefullDatabaseAsync();
using (IServiceScope scope = host.Services.CreateScope())
{
var resources = scope.ServiceProvider.GetRequiredService<IResourceServiceClient>();
IResourceContainerClient reports = await resources.CreateContainerIfNotExistsAsync("reports");
await using FileStream file = File.OpenRead("summary.csv");
await reports.UploadAsync("summary.csv", file, new ResourceUploadOptions { Mode = ResourceWriteMode.Replace });
}
Next steps
- Going to production: what to set up before real users and real data arrive.
- Uploads and downloads for streams, lengths, hashes and cancellation.
- ASP.NET Core for serving files from an API with the right status codes and headers.
- The samples: a file service shaped like a production deployment, a minimal API and a Blazor app, which all run out of the box.