Skip to content
New Livestream: OWASP Top 10 2025 Explained for .NET Developers. Register Now!

Databases Supported by Duende Storage

Duende Storage supports SQL Server, PostgreSQL, Oracle and SQLite. This page compares them, explains how provider registration and schema deployment work the same way across all of them, and links to each provider’s page for connection strings, custom schemas and provider-specific notes.

DatabasePackageRegistration methodConnection object you registerSchema optionCLI --provider valueIntended use
SQL ServerDuende.Storage.MsSqlAddMsSqlCreateSqlConnection factorySchemaNamemssqlProduction
PostgreSQLDuende.Storage.PostgreSqlAddPostgreSqlNpgsqlDataSourceSchemaNamepostgresqlProduction
OracleDuende.Storage.OracleAddOracleCreateOracleConnection factorySchemaNameNot supported yet (run migrations without the CLI)Production
SQLiteDuende.Storage.SqliteAddSqliteA connection stringNonesqliteAutomated tests and local development only

Duende Storage doesn’t recommend any particular database. Choose the database that fits your team’s operations and hosting environment. SQL Server, PostgreSQL and Oracle are all production-ready; SQLite is well-suited for automated tests and local development only.

Every provider follows the same registration shape:

  • AddStorage(...) selects exactly one database provider for a storage instance. Calling it with a second provider for the same instance throws an InvalidOperationException.
  • For SQL Server, PostgreSQL and Oracle, your application owns the connection factory or data source: a CreateSqlConnection or CreateOracleConnection delegate, or an NpgsqlDataSource. You control credentials, token providers and pooling settings on it. Duende Storage never disposes an object you registered this way.
  • The default-instance overloads (for example AddMsSql(Action<MsSqlStorageEngineOptions>)) resolve that object unkeyed from dependency injection, and only work for the default storage instance. Named instances must use the resolver overload (for example AddMsSql(Func<IServiceProvider, CreateSqlConnection> resolver, ...)), which resolves the object however you configure it, including from a keyed registration. See Multiple Storage Instances for how to register and migrate more than one storage instance.

Duende Storage separates two kinds of schema change:

  • Document-level changes, such as adding a custom property to a client or resource, need no migration. The data extension schemas validate documents, not database tables.
  • The fixed set of storage tables (entities, indexes, the outbox, and the schema version itself) is created and upgraded by Duende Storage’s own migrations, through the APIs below.

IStorageInstanceSchema gives you those migration operations for a storage instance:

  • MigrateAsync() creates the schema if it doesn’t exist, upgrades it if it’s behind, or does nothing if it’s already current. It then calls VerifySchemaAsync and throws an InvalidOperationException if verification finds errors.

  • CheckVersionAsync() returns a CheckSchemaVersionResult with CurrentVersion, RequiredVersion and IsCompatible, so you can fail fast before MigrateAsync instead of discovering a mismatch at request time:

    Program.cs
    using Duende.Storage.Schema;
    var schema = app.Services.GetRequiredService<IStorageInstanceSchema>();
    var versionCheck = await schema.CheckVersionAsync(CancellationToken.None);
    if (!versionCheck.IsCompatible)
    {
    throw new InvalidOperationException(
    $"Database schema version {versionCheck.CurrentVersion} does not match the " +
    $"required version {versionCheck.RequiredVersion}.");
    }

    IsCompatible is false when CurrentVersion and RequiredVersion differ. An unreachable database throws an exception rather than returning a result with IsCompatible set to false:

    • CurrentVersion of 0 means the database has never been migrated: run the migration.
    • CurrentVersion less than RequiredVersion means the database is behind: run the migration.
    • CurrentVersion greater than RequiredVersion means a newer version of Duende Storage already migrated the database. Upgrade the application to a version that expects the current schema instead of trying to downgrade the schema.
  • VerifySchemaAsync() checks that every table, index and other object the current schema version expects is present, without changing anything.

  • BuildMigrationScript(DatabaseSchemaVersion fromVersion) returns the SQL for the given starting version without opening a database connection. Pass DatabaseSchemaVersion.Zero for the full script from an empty database.

app.Services.GetRequiredService<IStorageInstanceSchema>() resolves the default storage instance only. For a named instance, use IStorageInstanceSchemaFactory.GetStorageInstanceSchema(storageInstanceId, cancellationToken) to resolve its schema, as shown in Multiple Storage Instances.

On SQL Server, PostgreSQL and SQLite, each version step runs inside a transaction, so a failed step rolls back and running the migration again resumes from the last completed version. On Oracle, DDL commits automatically, so a failed step can leave some objects already created; the migration scripts skip objects that already exist, so running the migration again is safe there too. After migrating, Duende Storage verifies the schema and throws if verification finds a mismatch.

Don’t commit connection strings or store them in plain configuration files. Options that keep secrets out of source control and shell history include:

  • Environment variables, for example ConnectionStrings__IdentityServer, which IConfiguration binds automatically.
  • Azure Key Vault or another vault-backed configuration provider.
  • dotnet user-secrets for local development.
  • A passwordless or managed-identity connection, where the driver supports one, so no password needs to be stored at all.

For the Duende CLI, prefer the DUENDE_STORAGE_CONNECTION_STRING environment variable over --connection-string, so the connection string doesn’t appear in shell history or process listings.

The SQL Server, PostgreSQL and Oracle drivers all pool connections by default and expose pool-sizing keywords on the connection string or connection object you register: Min Pool Size/Max Pool Size for SQL Server, Minimum Pool Size/Maximum Pool Size for PostgreSQL, and Min Pool Size/Max Pool Size for Oracle. Tune these on the connection string; see each provider’s page for the keywords it supports. SQLite is for automated tests and local development only, so connection pooling isn’t a concern there.