WestyxNexus.Extensions.DependencyInjection (v0.11.0+)
Bind Nexus secrets to a strongly-typed options class, with live reload on rotation - and without putting them into IConfiguration.
This is a default, not a restriction. Secrets stay out of That gives you the Azure Key Vault-style layout, with the values under a
IConfiguration unless you ask for them, and one parameter changes it:Secrets: prefix. Both paths are supported, documented and tested - pick the one your application needs. The rest of this page explains why the default is the other way round, and exactly what changes when you switch.NexusClient in the container - see Wiring the packages
together. builder.Configuration.AddWestyx(config) alone does not
register one, because it keeps its client inside the configuration provider; without a registered
client this call reports which registration is missing.
IOptions<T>, IOptionsSnapshot<T> and IOptionsMonitor<T> are all registered.
Declare the options type as a class, not a record
This is the one part of the path the SDK cannot secure for you, and it is worth thirty seconds of attention. Keeping the secret out ofIConfiguration protects the configuration tree; the object the
value is bound into is yours.
A C# record gets a compiler-generated ToString() that prints the names and values of all public
properties. So this compiles, binds, and writes your Stripe key to the log the first time anything
formats the options object:
sealed class, as
every example on this page does, and the same log line prints the type name only. If you want a
record for the value semantics, override ToString():
public record StripeOptions(string ApiKey);) will not work regardless: the
options pattern instantiates TOptions with Activator.CreateInstance<TOptions>(), which needs a
public parameterless constructor and throws MissingMethodException without one.
Why not just put secrets in IConfiguration?
If you know the Azure Key Vault configuration provider you will expect exactly that, so this deserves an explanation rather than a shrug.
.NET has no way to mark a configuration value as secret. Once a value is in the configuration tree:
- the parameterless
IConfigurationRoot.GetDebugView()prints it, together with the provider it came from, configuration.AsEnumerable()walks it,- so does any code that enumerates the configuration root - a startup configuration dump, a hand-written
/debug/configendpoint, a diagnostics or health package.
GetDebugView(Func<ConfigurationDebugViewContext, string>) overload, whose parameter is documented as “the function for processing the value, for example, hiding secrets”. It works - but only if the caller opts in, and an SDK cannot force that. On the conventional design, whether your Stripe key ends up in a log line depends on the discipline of every piece of code that touches the configuration root, including code you did not write.
AddWestyxNexusSecrets<T>() removes the exposure structurally instead. IOptions<T> does not require IConfiguration - it can be satisfied by a factory - so the value never enters the configuration tree at all. You keep constructor injection, strong typing and live reload; you lose only the ability to dump it by accident.
Key mapping
Secrets are matched by prefix. The remainder of the key becomes the property path.
Nexus keys are snake_case by convention and .NET properties are PascalCase. The binder handles that mapping, so
api_key finds ApiKey. Matching is case-insensitive.
Live re-bind
Every registration subscribes to the client’s post-sync notification, so a rotated secret reachesIOptionsMonitor<T>.CurrentValue on the next sync - within milliseconds when the SSE stream is connected.
Secrets in IConfiguration (the Key Vault-style layout)
To have secrets reachable through IConfiguration - the layout the Azure Key Vault provider gives you:
Secrets: prefix, so stripe.api_key becomes
configuration["Secrets:stripe:api_key"], binds with GetSection("Secrets:stripe"), and
participates in normal configuration layering.
This is a supported configuration, not a workaround. What changes is one thing, and it is the reason
the default is the other way round: the values are now part of the configuration tree, so
GetDebugView(), AsEnumerable() and anything else that enumerates it can reach them.
A masking helper ships for exactly that case:
What was rejected
ANexusSecretValue wrapper type whose ToString() returns "[REDACTED]" was considered and turned down. It is a breaking change to GetSecret, and in the case that actually matters - a secret bound into a POCO string property - it provides no protection at all. A half-measure that creates false confidence is worse than none.
