Repository navigation
Registering a category's default provider before AddEssentials() makes the interface resolve to a different provider (JSON→TOML, Crypto→PCG, Gzip→ZLib, …) #149
Description
What's wrong
AddEssentials() makes a provider the default for its category by registering it last, because resolving a single IX returns the last registration. You can see this in Essentials.All/ServiceCollectionExtensions.cs, at the comments "Registered last so that it is the provider an IX resolves to" (around L134-135, L186-187, L201-202, L241-242, L257-258, L297-299).
The default providers themselves register with TryAddSingleton + TryAddEnumerable, for example:
Essentials.SerializationProviders.Json/ServiceCollectionExtensions.csL47-57Essentials.RandomProviders.Crypto/ServiceCollectionExtensions.csEssentials.CompressionProviders.Gzip/ServiceCollectionExtensions.cs
So if the caller has already registered the default, the "last" call inside AddEssentials() adds nothing. The providers registered before it in the same helper still go in after the caller's registration. One of them becomes the last registration, and the interface resolves to it.
The README tells users to do exactly this for JSON options (README.md ~L248: "Register it before AddEssentials(), which would otherwise register the default one first"):
services.AddJsonSerializationProvider(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
services.AddEssentials();
provider.GetRequiredService<ISerializationProvider>(); // TomlSerializationProvider, not JSONThat brings #87 back through the documented setup. The caller's options are never used, and the FileSystem, ConfigHome, DataHome and Temp persistence providers silently write TOML (see #69).
The same thing happens in every category whose default is chosen by registration order:
Registered before AddEssentials() |
Interface resolves to |
|---|---|
AddJsonSerializationProvider(...) |
TomlSerializationProvider |
AddCryptoRandomProvider() |
PcgRandomProvider, a non-cryptographic generator, although the caller explicitly asked for the CSPRNG |
AddGzipCompressionProvider() |
ZLibCompressionProvider (Deflate on netstandard2.1) |
AddSHA256HashProvider() |
XxHash128HashProvider |
AddBase64EncodingProvider() |
HexEncodingProvider |
AddHmacSha256KeyedHashProvider() |
HmacSha512KeyedHashProvider |
PR #136 (#132) already fixed this for the parameterized providers (Xor, Caesar, BitRotate, distributions). It routes them through ProviderRegistration.AddDefaultOrKeep (Shared/ProviderRegistration.cs L55-91), which moves an earlier registration to the default position. The six category defaults above, including the JSON overload that takes options, still use plain registrations.
A minimal DI model with the same TryAddSingleton/TryAddEnumerable shapes reproduces it: register JSON with an instance, then run the TOML and JSON registrations in AddEssentials() order, and GetRequiredService<ISerializationProvider>() returns TOML.
Suggested fix
- In
AddEssentials(), route every "registered last" default throughAddDefaultOrKeep<T>, the same way the parameterized providers already are. For example:.AddDefaultOrKeep<JsonSerializationProvider>(static s => s.AddJsonSerializationProvider()). Do the same forCryptoRandomProvider,GzipCompressionProvider,SHA256HashProvider,Base64EncodingProviderandHmacSha256KeyedHashProvider. - Optionally, have the JSON overload that takes options use
ProviderRegistration.AddOrReplaceSingleton. It would then also work afterAddEssentials(), and the README caveat could go.
Acceptance criteria
- For each of the six categories,
services.Add<Default>(); services.AddEssentials();resolves that default for the interface. ExtendDefaultProviderTests.AddEssentials_Resolves_The_Documented_Default_Per_Categorywith this call order. -
AddJsonSerializationProvider(options)followed byAddEssentials()resolves a JSON provider that usesoptions. -
AddEssentials_Lists_Each_Implementation_Oncestill passes.
Activity
matt-edmondson commentedon Oct 19, 2026
Triage
AddCryptoRandomProvider()ends up resolving to the non-cryptographic PCG generator. The last case is security-relevant for anyone generating keys or tokens.Essentials.All/ServiceCollectionExtensions.cs(AddEssentials) and the six category-defaultAdd*Providerregistrations. Owner: @matt-edmondsonProviderRegistration.AddDefaultOrKeep) covered only the parameterized providers. No open PR addresses it.AddDefaultOrKeep, and add a register-before-AddEssentials()test per category asserting what the interface resolves to.Generated by Claude Code
Keep a category default registered before AddEssentials() the default