Skip to main content
This SDK departs from a few things a .NET developer would reasonably expect. Each departure is deliberate; none is an oversight. They are written down here so you can judge them rather than discover them.

1. Secrets are not in IConfiguration by default

The most important entry on this page. If you know the Azure Key Vault configuration provider, you will expect Nexus secrets to appear in IConfiguration the same way. They do not, and the reason is mechanical rather than stylistic. .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/config endpoint, a diagnostics or health package.
There is a GetDebugView(Func<ConfigurationDebugViewContext, string>) overload that can mask values by originating provider - ConfigurationDebugViewContext exposes the ConfigurationProvider, and the parameter is documented as “the function for processing the value, for example, hiding secrets”. It only helps if the caller opts into it, 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. Our path removes the exposure structurally rather than by convention: IOptions<T> does not require IConfiguration, so AddWestyxNexusSecrets<T>() satisfies it through a factory and the value never enters the configuration tree. You still get constructor injection, strong typing and live reload - you just cannot accidentally dump it. This is a default, not a restriction. AddWestyx(..., includeSecrets: true) gives you the Key Vault-style layout in one parameter, with a GetNexusSafeDebugView() masking helper for the configuration dumps that then become a risk. Both paths are supported and tested. See Secrets binding for what changes when you switch. One part of this is yours, not ours: the options type the secret is bound into. A C# record generates a ToString() that prints every public property, so declaring the options type as a record puts the value one logger.LogInformation("{Options}", ...) away from the log. Declare it as a sealed class, or override ToString() - see Secrets binding.

2. NexusSecretValue was considered and rejected

A wrapper type whose ToString() returns "[REDACTED]" was proposed 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 whatsoever. A half-measure that creates false confidence is worse than no measure.

3. Extension methods live in Microsoft.Extensions.DependencyInjection

Microsoft’s guidance says extension methods should not be placed in that namespace unless you are authoring an official Microsoft package. We deviate knowingly, because the rest of the ecosystem already does: Serilog, Polly and the AWS and Azure SDKs all register their Add* extensions there. Following the letter of the guidance while every neighbouring package ignores it would mean AddWestyxNexus() fails to appear when you type services. with your usual usings - which is exactly the discoverability problem this release set out to fix.

4. Logging is Microsoft.Extensions.Logging only, and never a configuration-bindable property

NexusConfig.LoggerFactory takes a standard ILoggerFactory, and each subsystem gets its own category (Westyx.Nexus.NexusClient, Westyx.Nexus.Stream, Westyx.Nexus.Wif) so the SSE stream can be turned up to Debug without every sync coming with it. NexusOptions - the configuration-bindable form used by AddWestyxNexus() - has no LoggerFactory or Observer property, for the same reason it has no HttpClient: a service reference cannot be constructed from appsettings.json, and a property the binder can never fill only produces warnings under a source-generated binder. Register the service in the container instead - services.AddSingleton<ILoggerFactory>(...), services.AddSingleton<IStreamObserver, MyObserver>() - and AddWestyxNexus() resolves it from there, falling back to the application’s own ILoggerFactory when none is registered.

5. Microsoft.Extensions.Configuration left the core package

This is a breaking change, taken because a console app or worker service should not carry the configuration pipeline it never touches. AddWestyx() now lives in WestyxNexus.Extensions.Configuration. Migration is one line in your project file - add the package reference. The namespace is still Westyx.Nexus, so no using changes.

6. PooledConnectionLifetime defaults to 2 minutes

SocketsHttpHandler.PooledConnectionLifetime defaults to InfiniteTimeSpan. Because NexusClient is meant to live for the lifetime of the application, that default means its pooled connections are never recycled and it never observes a DNS change - a real scenario behind an ingress or a load balancer. Two minutes matches the IHttpClientFactory default handler lifetime, so the standalone and dependency-injection paths behave the same. Tune it with NexusConfig.PooledConnectionLifetime; it is ignored when you supply your own HttpClient, because that client’s handler is yours to configure.

7. IHttpClientFactory handler rotation is safe for SSE

Rotating handlers every two minutes looks fatal for a connection that stays open for hours. It is not: the factory only stops handing an expired handler to new requests; a request already in flight keeps its handler until it completes. Do not “fix” this with SetHandlerLifetime(Timeout.InfiniteTimeSpan) - that reintroduces the stale-DNS problem the factory exists to solve.

8. FeatureManagement maps boolean state only

Only a flag’s is_active state reaches IFeatureManager. Percentage rollout and cohort targeting are evaluated server-side against a user context and are not routed through the bridge. A targeting-aware flag exposed through a boolean-only bridge would silently evaluate as its untargeted default, which is worse than being unsupported. See Feature management for how to evaluate targeted flags today.

9. Both target frameworks, and the net8.0 clock

The packages target net8.0 and net10.0. .NET 8 goes out of support on 2026-11-10; .NET 10 is supported to 2028-11-14. net8.0 is kept so consumers are not stranded mid-support-window, and will be dropped after that date.

10. File-type secrets are owner-readable only

Materialised file secrets are created with mode 0600, set at creation via UnixCreateMode rather than with a chmod afterwards - a chmod-after leaves a window in which the file exists with the process umask applied, typically 0644 in a shared temp directory. Each client instance also gets its own file path. The path used to be derived only from the secret’s key and value, so two clients holding the same secret resolved to one file and whichever was disposed first deleted it out from under the other. On Windows there is no equivalent mode; the file inherits the temp directory ACL.

11. The provider type is an enum in code and a string in configuration

WifConfig.Provider is a WifProvider enum: the set is closed, a switch over it is exhaustive, and a value outside it is rejected at construction rather than carried to the wire. WifOptions.Provider - the type IConfiguration binds - is a string, and the SDK converts it. The split is what makes "aws_iam" in appsettings.json mean the same thing however you build. Binding a string straight to an enum property is unsafe in .NET: "999" and "Aws,Gcp" bind silently to values the enum never declares, and the wire spelling aws_iam does not bind at all - only the C# member name does. A [TypeConverter] that would repair the last case makes the reflection binder and the source-generated binder produce different results for the same file, and the source-generated binder switches on automatically under PublishAot and PublishTrimmed. Converting inside the SDK removes the question entirely: aws_iam and AwsIam are both accepted, case-insensitively, and anything else throws an ArgumentException naming the property. This is a rule for the whole SDK, not only for WIF: no configuration-bindable options property is an enum. A test enforces it for options added later.

12. The credential extension point is an interface, not a delegate

IWifCredentialSource returns a WifCredential, which is either an OIDC token or a signed, server-bound request. A token-returning delegate structurally cannot express the second shape - which is the shape aws_iam uses - so with a delegate, adding a provider meant changing the SDK. An interface also resolves from the dependency-injection container, can gain members later without breaking implementers, and is mockable without a closure. Azure.Core.TokenCredential is an abstract class rather than an interface for a reason that does not apply here: it targets netstandard2.0, where default interface members do not exist, so an abstract class is its only way to add a member non-breakingly. This SDK’s floor is net8.0, where they do exist. The credential hierarchy itself is closed - a private constructor, and only the two nested cases derive from it - so the wire contract stays owned by the SDK.