ConfigurationStore.Client
0.1.4-prerelease
dotnet add package ConfigurationStore.Client --version 0.1.4-prerelease
NuGet\Install-Package ConfigurationStore.Client -Version 0.1.4-prerelease
<PackageReference Include="ConfigurationStore.Client" Version="0.1.4-prerelease" />
<PackageVersion Include="ConfigurationStore.Client" Version="0.1.4-prerelease" />
<PackageReference Include="ConfigurationStore.Client" />
paket add ConfigurationStore.Client --version 0.1.4-prerelease
#r "nuget: ConfigurationStore.Client, 0.1.4-prerelease"
#:package ConfigurationStore.Client@0.1.4-prerelease
#addin nuget:?package=ConfigurationStore.Client&version=0.1.4-prerelease&prerelease
#tool nuget:?package=ConfigurationStore.Client&version=0.1.4-prerelease&prerelease
ConfigurationStore.Client
.NET client for ConfigurationStore, a
self-hosted secrets and configuration manager. It adds a ConfigurationStore server as an
IConfiguration source, so secrets arrive as ordinary configuration values, feature flags land under
the FeatureManagement section, and files are downloaded on demand. When push is enabled the client
keeps a live connection and reloads configuration when it changes on the server.
Install
dotnet add package ConfigurationStore.Client --prerelease
Quick start
Add a ConfigurationStore section with the server URL and a per-environment API key (put the key in
user secrets or an environment variable, not in appsettings.json):
{
"ConfigurationStore": {
"ServerUrl": "https://config.example.com",
"ApiKey": "cfgs_...",
"ClientName": "orders-api",
"EnablePush": true
}
}
Then wire it up with one call on the host builder:
var builder = WebApplication.CreateBuilder(args);
builder.AddConfigurationStore();
This reads the ConfigurationStore section, registers the configuration source, and registers a
singleton ConfigurationStoreClient in DI. The server is contacted at startup: like a database-backed
app needs its database, the app needs the server to be reachable to start.
Every option binds from the section, so it can just as well come from environment variables —
ConfigurationStore__ServerUrl, ConfigurationStore__ApiKey, ConfigurationStore__ClientName,
ConfigurationStore__EnablePush — which is handy in containers. ClientName (default: the application
name) is how this instance shows up in the server's connected-clients list; set it to tell one consumer
apart from another. Anything you set through the AddConfigurationStore(options => …) callback overrides
the configured values.
Secrets
Secrets are plain configuration values, so read them however you already read configuration —
IConfiguration, or bind an options class with IOptions<T> / IOptionsMonitor<T>:
string connectionString = builder.Configuration["Database:ConnectionString"];
builder.Services.Configure<DatabaseOptions>(builder.Configuration.GetSection("Database"));
With push enabled, the provider raises change tokens on updates, so IOptionsMonitor<T> re-binds live.
Feature flags
Feature flags map to the FeatureManagement:{name} section, so they work with
Microsoft.FeatureManagement out of
the box:
builder.Services.AddFeatureManagement();
// elsewhere
if (await featureManager.IsEnabledAsync("NewCheckout"))
{
// ...
}
Flag changes reload live too, so evaluation reflects the current server state.
Files
Files are fetched on demand through the injected ConfigurationStoreClient:
public sealed class CertificateLoader(ConfigurationStoreClient configStore)
{
public async Task<byte[]?> LoadAsync()
{
ConfigurationStoreFile? file = await configStore.GetFileAsync("web-cert");
return file?.Content;
}
}
configStore.Files lists the file entries (key, file name, version) from the latest snapshot.
Reacting to changes
Beyond the automatic configuration reload, you can subscribe to change notifications. Both return an
IDisposable; dispose it to unsubscribe.
IDisposable configSub = configStore.OnConfigChanged(() =>
{
// a secret or feature flag changed
});
IDisposable fileSub = configStore.OnFileChanged(file =>
{
// this file changed on the server; re-download if you cache it
});
Set EnablePush to false to disable the live connection and use only the snapshot taken at startup.
Resilience and caching
The push connection reconnects automatically and indefinitely (capped backoff with jitter), so it survives a server redeploy rather than giving up after a short window. When it reconnects it re-fetches the snapshot, so any changes pushed while the client was disconnected are still picked up.
By default the client is in-memory only and the server must be reachable at startup — if the snapshot cannot be fetched, startup fails (like a database-backed app needs its database). This is the safest default: nothing is written to disk.
Opt-in on-disk caching
Enable caching so the app can start from a cached snapshot when the server is unreachable at startup. Because the cache holds secrets it is always encrypted, so you must supply both a store and a protector:
builder.AddConfigurationStore(options =>
{
options.Cache = new ConfigurationStoreCacheOptions
{
Store = new FileCacheStore(), // per-user directory by default
Protector = new DpapiCacheProtector(), // Windows; use AesGcmCacheProtector elsewhere
MaxCacheAge = TimeSpan.FromHours(24), // older entries are ignored (default 24h)
};
options.InitialFetchTimeout = TimeSpan.FromSeconds(5); // startup wait before falling back to cache
});
Startup behaviour with caching enabled:
| At startup | Result |
|---|---|
| Server reachable | Normal start; snapshot (and downloaded files) written to the cache |
| Server unreachable, usable cache present | Start from the cache (degraded); the connection keeps retrying and resyncs when the server returns |
| Server unreachable, no usable cache (first run, cleared, too old, corrupt) | Startup fails |
| API key rejected (401/403) | Startup fails immediately — a rejected key is a misconfiguration, never a cache fallback |
Downloaded files are cached on demand the same way: GetFileAsync returns the cached copy when the server
is unreachable (keyed by the file's version, so a changed file is re-fetched), and throws if no cached copy
exists.
Configuring the cache from configuration / environment
Caching can also be turned on entirely from configuration — no code — which is what you want in a container.
Set Cache:Enabled to true and provide the directory, protector and (for AES-GCM) key. When Cache:Enabled
is absent or false, every other Cache:* value is ignored and caching stays off; when it is true, startup
fails fast if the configuration is incomplete rather than silently degrading.
{
"ConfigurationStore": {
"ServerUrl": "https://config.example.com",
"ApiKey": "cfgs_...",
"EnablePush": true,
"Cache": {
"Enabled": true,
"Directory": "/data/cs-cache",
"Protector": "AesGcm", // AesGcm (default) or Dpapi (Windows only)
"Key": "<base64 32-byte key>", // required for AesGcm
"MaxCacheAge": "12:00:00" // optional, default 24h
}
}
}
The same via environment variables (e.g. a docker-compose environment: block) — no key file, since this
must be available before and without a reachable server:
services:
app:
image: myapp
environment:
ConfigurationStore__ServerUrl: "https://config.example.com"
ConfigurationStore__ApiKey: "cfgs_..."
ConfigurationStore__Cache__Enabled: "true"
ConfigurationStore__Cache__Directory: "/data/cs-cache"
ConfigurationStore__Cache__Protector: "AesGcm"
ConfigurationStore__Cache__Key: "<base64 32-byte key>"
The defaults are the same on Windows, macOS and Linux: Protector defaults to AesGcm, which requires a
32-byte Key. Dpapi is available as an override but only on Windows. Note the AES-GCM key sits in the
environment next to ApiKey — that is not a new exposure, since the ApiKey already grants access to every
value the server returns; the key only protects the on-disk cache against theft without the environment.
Protectors
DpapiCacheProtector(Windows) — the encryption key is managed by the OS and bound to the Windows user account, so there is nothing to provision. The strongest option on Windows.AesGcmCacheProtector(key)— portable AES-256-GCM with a 32-byte key you supply from wherever the app can reach it at startup without the server (environment variable, KMS, user secrets).FromBase64Keyis a convenience for a base64-encoded key.
Both use authenticated encryption, so a tampered cache entry fails to decrypt and is treated as absent.
Implement ICacheStore / ICacheProtector to plug in your own storage or key management.
What at-rest encryption does and does not protect
Because the app must decrypt the cache unattended, the key is reachable by the app. At-rest encryption therefore protects against file-level disclosure — disk theft, backups, other users on the machine, accidental exposure in logs or copies — but not against an attacker who can already run as the application (they can read the same secrets from process memory). Enabling the disk cache is a deliberate availability-vs-secrets-at-rest trade; the default (no disk cache, fail-fast) avoids it entirely.
Reading configuration from a script
The same resolved configuration is available over the HTTP retrieval API, so anything that can make an HTTP request can read it with a per-environment API key — no .NET client required. For example, from PowerShell:
$server = 'https://config.example.com'
$headers = @{ 'X-Api-Key' = $env:CONFIGURATIONSTORE_API_KEY }
# The resolved snapshot: secrets/settings under .strings, feature flags under .flags,
# file entries (key, fileName, version) under .files.
$config = Invoke-RestMethod -Uri "$server/api/config" -Headers $headers
# Keys can contain ':', so quote them when they do.
$connectionString = $config.strings.'Database:ConnectionString'
$newCheckoutEnabled = $config.flags.NewCheckout # $true / $false
# Download a file entry by key.
Invoke-WebRequest -Uri "$server/api/config/files/web-cert" -Headers $headers -OutFile 'web-cert.pfx'
dotenv output
Add ?format=dotenv to get the snapshot as KEY=value lines (text/plain) instead of JSON, so a script
can write an .env file directly — useful for feeding a docker-compose app you cannot modify, with no jq:
curl -s -H "X-Api-Key: $CONFIGURATIONSTORE_API_KEY" \
"https://config.example.com/api/config?format=dotenv" > .env
docker compose up -d
Format details:
- Keys: hierarchical
:separators are written as__(e.g.Database:ConnectionStringbecomesDatabase__ConnectionString), which is a valid environment-variable name and is mapped back to:by .NET's environment-variable configuration provider. Keys are sorted for stable output. - Values use standard dotenv quoting: simple values are emitted bare, and values containing spaces or
other special characters are double-quoted with
\\,\",\n,\rand\tescapes. An empty value is written asKEY="". - Feature flags are written flat as
KEY=true/KEY=false. Use?format=dotenv-netinstead to place them under theFeatureManagement__section so Microsoft.FeatureManagement reads them straight from the environment. - Files carry no inline value (their content is fetched separately via
/api/config/files/{key}); they are listed as trailing# file: <key> (<fileName>)comments so you know they exist.
Notes
- No disk cache by default. The client is in-memory only unless you opt in to caching (see Resilience and caching); otherwise the server must be reachable at startup.
- Logging. The client is created during configuration bootstrap, before the DI logging pipeline exists,
so pass an
ILoggerFactoryviaoptions.LoggerFactoryto capture connection, reconnect and cache diagnostics.
| Product | Versions 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. |
-
net8.0
- Microsoft.AspNetCore.SignalR.Client (>= 8.0.11)
- Microsoft.Extensions.Configuration (>= 8.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
- System.Security.Cryptography.ProtectedData (>= 8.0.0)
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 |
|---|