PostgreSQL
Resourcefull.PostgreSql stores resources in a PostgreSQL database, splitting content into chunks so files of any
size stream in and out with flat memory use. It's tested against PostgreSQL 16 and 17 on every change.
dotnet add package Resourcefull.PostgreSql
using Resourcefull;
using Resourcefull.PostgreSql;
builder.Services.AddResourcefull(options => options
.UsePostgreSql(builder.Configuration.GetConnectionString("Resources")!));
It does everything the SQL Server provider does, including versions, conditional writes, copies and moves, expiry, container policies, and export and import, and it passes the same contract tests. This page covers what's particular to PostgreSQL; Provider features compares the providers.
Choosing the database
The connection string names the database that holds Resourcefull's tables: ResourceContainers, Resources,
ResourceChunks and ResourceMetadata, plus EF Core's migration history, __EFMigrationsHistory. It can be a database
of its own or your application's, as long as none of your tables share those names, or Resourcefull's tables are in a
schema of their own. Anything Npgsql accepts in a connection string works; keep it out of source
control, in user secrets, environment variables or a secrets store.
Choosing a schema
Without a schema, Resourcefull's tables go where the connection's search path puts them, normally public, beside your
own. To keep them apart, give UsePostgreSql a schema:
builder.Services.AddResourcefull(options => options
.UsePostgreSql(connectionString, npgsql => { }, schema: "resourcefull"));
Resourcefull then qualifies its tables' names with that schema, as in resourcefull."Resources", so they go there.
So does its migration history, resourcefull."__EFMigrationsHistory", which it then no longer shares with your own
EF Core migrations. MigrateResourcefullDatabaseAsync creates the schema if it's missing. Because the names
are qualified rather than found through the search path, this works behind a connection pooler such as PgBouncer too.
A schema's name is a letter or underscore, then up to 62 letters, digits or underscores. Every instance of the
application must use the same schema: an instance configured without it would look for the tables in public.
Provisioning
Provision the database once, before the application uses it, in one of two ways.
From the application. MigrateResourcefullDatabaseAsync creates the database if it doesn't exist and applies the
migrations it's missing; on an up-to-date database it does nothing:
var app = builder.Build();
await app.Services.MigrateResourcefullDatabaseAsync();
Run it from a deployment step, or at the startup of a single instance.
With a script. For a database administrator to run, have the application write the script, using the configuration it runs with:
File.WriteAllText("resourcefull.sql", app.Services.GenerateResourcefullScript());
Create the database, then run the script connected to it:
psql "host=<server> dbname=<database>" -v ON_ERROR_STOP=1 -f resourcefull.sql
-v ON_ERROR_STOP=1 stops at the first error. The script applies only the migrations the database is missing, so
it's safe to run again. With a schema, every table name in it is qualified with that schema.
Permissions
| Task | Needs |
|---|---|
| Running the application | SELECT, INSERT, UPDATE and DELETE on Resourcefull's tables, and USAGE on their schema |
| Provisioning or upgrading | Ownership of the tables or CREATE on their schema; CREATE on the database, if it has to create the schema; and CREATEDB, if MigrateResourcefullDatabaseAsync should create the database |
So the application can run with a least-privileged role, and a deployment step with a stronger one provisions the database.
How it differs from SQL Server
- Times are kept to the microsecond, PostgreSQL's precision. An expiry, or an imported version's
LastModified, given more precisely is stored with the rest cut off: a write returns it as given, and later reads return it cut. The times Resourcefull sets itself, such as when a version was stored, are whole microseconds already, so they read back exactly as the write returned them. - Listings order names by Unicode code point, PostgreSQL's byte order. It matches ordinal order (
StringComparer.Ordinal) except between characters beyond U+FFFF, such as emoji, and those from U+E000 to U+FFFF: PostgreSQL lists😀(U+1F600) after�(U+FFFD), where ordinal order lists it before. A listing still returns every name exactly once. See Listing resources. - Text can't hold U+0000, the null character, in PostgreSQL. No stored name or version ID has one, so a lookup of
one finds nothing. Storing a version whose name, version ID, content type or origin has one, such as a name from an
older release's export, throws
ArgumentException, as does setting such a content type. A copy or move to a container whose name has one reports that the container doesn't exist. - A write locks its name with an advisory lock,
pg_advisory_xact_lock, which ends with its transaction, so writers of a name take turns. The lock's key is a 64-bit hash of the name, in the same key space as any advisory locks your application takes; a collision only makes one wait for the other.
Retries
UsePostgreSql turns on EF Core's retry policy for PostgreSQL's transient errors, including deadlocks (40P01) and
serialization failures (40001): up to 5 retries, at most 10 seconds apart. Change it, or the command timeout, with
the configure callback:
builder.Services.AddResourcefull(options => options
.UsePostgreSql(connectionString, npgsql => npgsql.CommandTimeout(60)));
Replacing the retry policy, such as with EnableRetryOnFailure, also stops Resourcefull from counting retries in
resourcefull.postgresql.retries.
Health checks
builder.Services.AddHealthChecks().AddResourcefullPostgreSqlCheck(tags: ["ready"]);
It reports whether the database can be reached and has every migration this release needs, as the
SQL Server check does, with postgresql as the provider in its data.
Telemetry
Spans and metrics carry resourcefull.provider postgresql. The provider's own measurements are
resourcefull.postgresql.retries, with the SQLSTATE code in db.response.status_code, and
resourcefull.postgresql.abandoned_uploads, uploads that stopped partway and that the expiry sweep
deleted. See Telemetry.