Moberg.DbConfig.Provider.SqlServer 0.11.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package Moberg.DbConfig.Provider.SqlServer --version 0.11.1
                    
NuGet\Install-Package Moberg.DbConfig.Provider.SqlServer -Version 0.11.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Moberg.DbConfig.Provider.SqlServer" Version="0.11.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Moberg.DbConfig.Provider.SqlServer" Version="0.11.1" />
                    
Directory.Packages.props
<PackageReference Include="Moberg.DbConfig.Provider.SqlServer" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Moberg.DbConfig.Provider.SqlServer --version 0.11.1
                    
#r "nuget: Moberg.DbConfig.Provider.SqlServer, 0.11.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Moberg.DbConfig.Provider.SqlServer@0.11.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Moberg.DbConfig.Provider.SqlServer&version=0.11.1
                    
Install as a Cake Addin
#tool nuget:?package=Moberg.DbConfig.Provider.SqlServer&version=0.11.1
                    
Install as a Cake Tool

DbConfig

Database-backed IConfiguration provider for .NET with an embedded React editor UI.

NuGet NuGet NuGet NuGet NuGet Docs

Mimics the ergonomics of a secrets manager, but persists configuration in your existing application database. No additional external service required.

Packages

Package Purpose
Moberg.DbConfig.Core IConfigurationSource / IConfigurationProvider, IConfigStore abstraction, options
Moberg.DbConfig.Http JSON API endpoints (MapDbConfigHttp); auth is host-owned via RequireAuthorization
Moberg.DbConfig.Ui React editor UI shipped as embedded static assets (MapDbConfigUi); optional built-in cookie login since v0.10.0
Moberg.DbConfig.Provider.SqlServer SQL Server EF Core provider + dialect specifics
Moberg.DbConfig.Provider.PostgreSql PostgreSQL (Npgsql) EF Core provider + dialect specifics

Try it out

A runnable sample app — multi-tenant payments processor — lives at samples/PaymentsApi/. Demonstrates per-tenant config overrides, IOptionsSnapshot<T> binding, at-rest encryption, audit log, and live reload via the embedded admin UI.

cd samples/PaymentsApi
docker compose up -d
dotnet run

Then open http://localhost:5000/admin/dbconfig — the UI loads all your entries immediately and the filter fields in the toolbar narrow by AppName, Environment, or Tenant.

Security

Per-entry encryption via IsSecret flag:

  • Mark sensitive entries (IsSecret = true) → encrypted at rest using ASP.NET Core Data Protection (default). Non-secret values stay plaintext for debuggability.

  • The default Data Protection key ring is ephemeral and process-scoped. For multi-instance or restart-stable deployments, persist keys BEFORE AddDbConfig:

    builder.Services.AddDataProtection()
        .PersistKeysToFileSystem(new DirectoryInfo("/var/dbconfig/keys"))
        .ProtectKeysWithCertificate("thumbprint");
    builder.AddDbConfig(b => { ... });
    
  • Custom key management (Azure Key Vault, AWS KMS, etc.): register a custom IConfigEncryptor in builder.Services BEFORE AddDbConfig. Both instance and type-mapped registrations work in v0.6.0:

    // Type-mapped (DI resolves dependencies)
    builder.Services.AddSingleton<IConfigEncryptor, MyAzureKeyVaultEncryptor>();
    builder.AddDbConfig(b => { ... });
    
    // Or instance-registered (when you already have an instance)
    builder.Services.AddSingleton<IConfigEncryptor>(myEncryptorInstance);
    builder.AddDbConfig(b => { ... });
    

    Type-mapped caveat: the polling provider's decryption is deferred until host construction completes (an IHostedService resolves the encryptor and activates decryption). Reading secret config values BEFORE host.RunAsync() (or host.StartAsync()) is unsupported and throws a clear InvalidOperationException. Reading non-secret values pre-build is unaffected. Most code reads config from request handlers or hosted services, which run after build, so this is rarely hit in practice.

  • Non-secret values are NOT encrypted by design — feature flags, polling intervals, log levels stay plaintext for psql / SSMS debugging convenience.

Getting started

// 1. Single call — wires services, configuration source, and reload signal
builder.AddDbConfig(b =>
{
    b.Options.AppName = "MyApp";
    b.Options.Environment = builder.Environment.EnvironmentName;
    b.Options.ReloadInterval = TimeSpan.FromSeconds(30);
    b.UseSqlServer(connectionString); // or b.UsePostgreSql(connectionString)
});

// 2. Map the admin surface (UI + API under one prefix, one cookie)
builder.Services.AddScoped<IDbConfigCredentialValidator, MyValidator>();
app.MapDbConfigAdmin("/admin/dbconfig", opts =>
{
    opts.UseBuiltInLogin<MyValidator>();
});
// → UI at  /admin/dbconfig
// → API at /admin/dbconfig/api

AddDbConfig is an extension on IHostApplicationBuilder, so it works for both WebApplicationBuilder (ASP.NET Core) and HostApplicationBuilder (worker services / generic host).

The connection string must be present before AddDbConfig is called. If it is missing, an InvalidOperationException is thrown at startup — the provider does not silently return empty values.

See samples/PaymentsApi/ for a full working example — multi-tenant payments processor demonstrating per-tenant overrides, IOptionsSnapshot<T> binding, at-rest encryption, audit log, and live reload via the embedded admin UI.

Authentication

MapDbConfigAdmin, MapDbConfigHttp, and MapDbConfigUi are all open by default and return RouteGroupBuilder. Five supported patterns:

Pattern When
MapDbConfigAdmin(prefix, opts => opts.UseBuiltInLogin<T>()) Recommended — one cookie covers UI + API
Open (no auth) Private network, dev only
.RequireAuthorization("policy") The host already has OIDC / Windows Auth / JWT
Split prefixes + opts.UseBuiltInLogin<T>() UI behind CDN, API at different origin
opts.Authorization = new MyFilter() Header-based, IP allowlist, custom JWT cookie

The unified MapDbConfigAdmin is the common case as of v0.10.0 — one call mounts UI and HTTP API under one prefix with one shared cookie, so the React app can call its own backend right after sign-in.

See Authentication & authorization for the full walkthrough.

Programmatic access

Most code reads config through IOptionsSnapshot<T> (tenant-aware automatically) or IConfiguration. For admin endpoints, background jobs, or diagnostic surfaces that need to read explicitly, IConfigStore exposes ergonomic overloads that pick up AppName/Environment from DbConfigOptions and the current tenant from ITenantResolver:

// Single key — current tenant or global.
var entry = await store.GetAsync("Logging:Level", ct);

// Specific tenant.
var key = await store.GetForTenantAsync("Acme", "Stripe:ApiKey", ct);

// Typed POCO bind — section name is typeof(T).Name verbatim.
// "StripeOptions:" prefix is matched; tenant entries override global defaults.
var stripe = await store.GetForTenantAsync<StripeOptions>("Acme", ct);

Section name is typeof(T).Name verbatimStripeOptions reads from StripeOptions: keys, not Stripe:. This intentionally diverges from the standard ASP.NET Core services.Configure<StripeOptions>(GetSection("Stripe")) convention, which uses the short name. The typed IConfigStore overloads are for programmatic / cross-tenant reads — keep using IOptionsSnapshot<T> for current-tenant reads in request handlers. See the docs page for the two viable naming patterns (type-name-matches-section vs parallel namespaces).

See Programmatic access to IConfigStore for the full walkthrough.

Migrations

DbConfig owns its own schema (tables DbConfig_Entries and DbConfig_AuditEntries).

Default — auto-create on startup

The library applies pending migrations during AddDbConfig. No extra code needed:

builder.AddDbConfig(b =>
{
    b.UseSqlServer(connStr);
    b.Options.AppName = "MyApp";
    // b.Options.SchemaMode = SchemaMode.CreateIfMissing;  // default
});

This mirrors how Hangfire, Marten, and Wolverine handle schema. Good for dev, demos, and small-team production.

DBA-controlled / CI-pipeline workflows

Production teams that prefer to apply schema out of band (via init container, SQL review by DBA, CI/CD step) can disable auto-create:

builder.AddDbConfig(b =>
{
    b.UseSqlServer(connStr);
    b.Options.AppName = "MyApp";
    b.Options.SchemaMode = SchemaMode.None;  // host assumes schema is ready
});

To extract SQL for offline application:

// In a tiny build-time helper console app:
using DbConfig.EntityFrameworkCore;
using DbConfig.Provider.SqlServer;

var opts = SqlServerDbConfigOptions.ForSqlServer(connStr);
var sql = DbConfigMigrator.GenerateMigrationScript(opts, idempotent: true);
File.WriteAllText("dbconfig-upgrade.sql", sql);

DbConfigMigrator exposes three methods:

  • MigrateAsync(opts) — apply pending migrations programmatically
  • GenerateCreateScript(opts) — full schema DDL for a fresh database
  • GenerateMigrationScript(opts, fromMigration?, toMigration?, idempotent: true) — incremental upgrade SQL

For PostgreSQL hosts use PostgreSqlDbConfigOptions.ForPostgreSql(connStr).

Shared scopes

To pull configuration from one or more shared scopes in addition to your app's own:

builder.AddDbConfig(b =>
{
    b.UseSqlServer(connectionString);
    b.Options.AppName = "PaymentService";
    b.Options.Environment = builder.Environment.EnvironmentName;
    b.Options.IncludeScopes = ["PlatformDefaults", "Shared"];
    // Precedence (lowest → highest): PlatformDefaults < Shared < PaymentService
    // Own scope (AppName) always wins ties.
});

The polling provider reads from all listed scopes in one DB query and merges them with the configured precedence. A change in any included scope advances the watermark and triggers reload across all consumers within one poll interval.

Per-scope authorization (host pattern):

// App-team writes — only own scope
app.MapDbConfigHttp("/api/dbconfig", scopeFilter: "PaymentService")
   .RequireAuthorization("AppTeamAdmin");

// Platform-team writes — only Shared scope
app.MapDbConfigHttp("/api/dbconfig-shared", scopeFilter: "Shared")
   .RequireAuthorization("PlatformAdmin");

When scopeFilter is set, the group rejects writes (and reads) to other AppNames with 403. The /reload endpoint is always allowed.

Audit log

Every mutation (Upsert/Delete) writes a row to DbConfig_AuditEntries in the same transaction. The UI's per-row "History" button surfaces this; programmatic access via GET /{appName}/{environment}/audit/{*key}?take=50 returns ConfigAuditEntry[].

Audit log values are encrypted-at-rest using the same IConfigEncryptor as the main store. The history endpoint decrypts for the response, so callers see plaintext.

Disable per-host with b.Options.EnableAuditLog = false. Retention is the consumer's responsibility — recommended DELETE FROM DbConfig_AuditEntries WHERE ModifiedUtc < NOW() - INTERVAL '90 days' on a schedule.

Read auditing (opt-in)

By default DbConfig audits only mutations (Insert/Update/Delete). For compliance scenarios that require "who read this secret?" trails, enable read auditing:

builder.AddDbConfig(b =>
{
    b.UseSqlServer(connStr);
    b.Options.AppName = "PaymentService";
    b.Options.AuditReads = true;   // NEW in v0.6.0
});

When enabled, HTTP GET /{app}/{env} and GET /{app}/{env}/{*key} write fire-and-forget audit rows with Action=Read. Old/New values are null (the read itself isn't a state change). Failures to write the audit row log a warning; the GET still returns successfully.

Read audit rows are written for both 200 and 404 responses — a key probe is recorded even when the key doesn't exist. This is intentional for compliance posture (record access attempts, not just successful accesses).

The audit-history endpoint never generates read audits (no recursion).

Reload semantics

The configuration provider polls the store on a configurable interval (default 30 s). When the highest-watermark ModifiedUtc in the store advances, the provider fires an IChangeToken, which triggers IOptionsMonitor callbacks in the consuming application.

Important: Direct SQL DELETE on the DbConfig_Entries table will not be reflected by the polling provider until another row's ModifiedUtc advances. Always mutate via the API — the HTTP DELETE/PUT endpoints fire the in-process reload signal. Direct DB writes from migrations or DBA tools are not first-class in v0.1.0.

The HTTP POST /reload endpoint (mapped by MapDbConfigHttp) triggers an immediate in-process reload without waiting for the next poll interval.

Theming

The UI editor supports light and dark themes. Toggle via the sun/moon button in the page header; choice persists to localStorage. The Docusaurus docs site has its own light/dark toggle (top-right navbar). See website/docs/ui-editor/theming.md for implementation details.

Documentation

Full documentation lives under website/ (Docusaurus 3.10). To browse locally:

cd website
npm install
npm run start         # http://localhost:3000

Or build static HTML:

cd website
npm run build
npm run serve         # serve build/ on http://localhost:3000

UI screenshots in the docs are produced by a Playwright suite against a deterministic demo-mode of the UI. To regenerate them:

cd ui
npm run screenshots:install   # one-time: playwright install chromium
npm run screenshots           # produces 10 PNGs in website/static/img/screenshots/

The screenshots cover all v0.x features: entries list, editing, history with diff, bulk operations, import/export, scope selector, and the access warning banner.

Status

  • v0.6.0 (2026-05-17): Opt-in read auditing; UI features (diff view, bulk edit, import/export); type-mapped IConfigEncryptor registrations.

v0.5.0 (production hardening) — production-ready for the following scope:

  • SQL Server and PostgreSQL via EF Core
  • Hierarchical keys, App + Environment scoping
  • Polling-based reload with immediate-reload signal
  • Embedded React editor UI with CRUD, secret masking, scope badge, view-mode toggle, and per-row audit history
  • Host-owned authorization (no auth baked into the package)
  • Moberg.DbConfig.EntityFrameworkCore extracted from Core — consumers writing custom non-EF stores no longer pull the EF transitive dependency
  • IUniqueConstraintDetector strategy — provider-specific exception handling lives in the provider package, not in the shared store
  • Single-call design — builder.AddDbConfig(b => ...) on IHostApplicationBuilder wires services, configuration source, and reload signal in one shot. No bridge dance, no second DI container.
  • IConfigStore.GetAsync for targeted single-key reads — HTTP GET single no longer scans the full app/environment scope
  • DbConfigOptions.IncludeScopes — pull config from one or more shared scopes in addition to your own, with explicit precedence ordering
  • MapDbConfigHttp(scopeFilter: "X") — per-scope authorization at the group level
  • Per-entry encryption via IConfigEncryptor (IsSecret = true → encrypted at rest via ASP.NET Core Data Protection)
  • Audit log (DbConfig_AuditEntries) with in-transaction writes, HTTP read endpoint, and UI History dialog
  • Case-sensitive binary collation on scope columns (closes collation mismatch between HTTP filter and DB query)

Known limitations:

  • No audit log retention pruner (manual cleanup documented; opt-in pruner deferred to v0.7.0+)
  • Direct DB mutations bypass the reload signal AND audit log (always mutate via API — see Reload semantics above)
  • Two EfCoreConfigStore instances per host (one for the polling provider, one for the HTTP layer) both pointed at the same DB. They share no in-process state by design — the DB is the source of truth and the reload signal coordinates cache invalidation. Custom IConfigStore impls (e.g. Redis) currently can't share a single instance across polling and HTTP; a UseCustomStore<T>() registration helper is tracked for v0.6.0.
  • Ephemeral Data Protection key ring by default (document PersistKeysToXxx — see Security section)
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.15.0 124 6/25/2026
0.14.1 114 6/3/2026
0.14.0 109 5/22/2026
0.13.0 105 5/21/2026
0.12.0 112 5/20/2026
0.11.2 100 5/20/2026
0.11.1 96 5/20/2026
0.11.0 105 5/20/2026
0.10.2 106 5/19/2026
0.10.1 106 5/19/2026
0.10.0 108 5/19/2026
0.9.0 114 5/18/2026