Skip to content

Registering a category's default provider before AddEssentials() makes the interface resolve to a different provider (JSON→TOML, Crypto→PCG, Gzip→ZLib, …) #149

Closed

Description

@matt-edmondson

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.cs L47-57
  • Essentials.RandomProviders.Crypto/ServiceCollectionExtensions.cs
  • Essentials.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 JSON

That 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 through AddDefaultOrKeep<T>, the same way the parameterized providers already are. For example: .AddDefaultOrKeep<JsonSerializationProvider>(static s => s.AddJsonSerializationProvider()). Do the same for CryptoRandomProvider, GzipCompressionProvider, SHA256HashProvider, Base64EncodingProvider and HmacSha256KeyedHashProvider.
  • Optionally, have the JSON overload that takes options use ProviderRegistration.AddOrReplaceSingleton. It would then also work after AddEssentials(), 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. Extend DefaultProviderTests.AddEssentials_Resolves_The_Documented_Default_Per_Category with this call order.
  • AddJsonSerializationProvider(options) followed by AddEssentials() resolves a JSON provider that uses options.
  • AddEssentials_Lists_Each_Implementation_Once still passes.

Activity

  1. matt-edmondson commented on Oct 19, 2026

    @matt-edmondson
    ContributorAuthor

    Triage


    Generated by Claude Code

  2. added a commit that references this issue on Oct 26, 2026
    5b659d6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions