DI031

Shared Implementation Registered Under Several Service Types

One implementation type registered under two or more different service types with the same singleton or scoped lifetime, as plain type registrations on the same service-collection flow.

Info Default severity · Code fix: No

Why it matters

Each registration is its own descriptor, and the container builds one instance per descriptor. AddSingleton<IReader, Store>() followed by AddSingleton<IWriter, Store>() reads like one shared Store but produces two: state written through one interface is invisible through the other, and anything the implementation owns — a timer, a connection, a cache — exists twice. The bug is silent, because both resolutions succeed and return a perfectly valid object.

README problem example

services.AddSingleton<IFeatureReader, FeatureStore>();
services.AddSingleton<IFeatureWriter, FeatureStore>();  // DI031: a second FeatureStore

README better pattern

services.AddSingleton<FeatureStore>();
services.AddSingleton<IFeatureReader>(sp => sp.GetRequiredService<FeatureStore>());
services.AddSingleton<IFeatureWriter>(sp => sp.GetRequiredService<FeatureStore>());

No — the repair chooses which service type keeps the concrete registration and rewrites the rest as factories, which changes registration order and is better made deliberately.

Guardrails

When DI031 stays silent

Transient registrations are exempt — a fresh instance per resolution is the contract, so there is no shared instance to lose. Registrations with different lifetimes, keyed registrations, factory registrations, pre-built instances, and registrations on different service-collection flows are all left alone, as is the same service type registered twice (that is DI012's duplicate registration). Registrations guarded by an if, switch, loop, or try never both run, so no two-instance claim is made, and neither do registrations in different executable bodies. Grouping is by constructed type, so GenericStore<int> and GenericStore<string> are distinct, and a RemoveAll or Replace that runs after the registration it removes withdraws the claim (a removal earlier in the method does not). Known false negatives: an open-generic registration paired with a closed one, and a fluent chain that removes a service type and then re-registers it in the same expression. Reported at Info: separate instances are occasionally deliberate, and the forwarding fix is a design decision.

Repo sample extraction

Examples pulled from the sample app

Open full sample file

Sample app shared-implementation note

        public static void ConfigureBad(IServiceCollection services)
        {
            services.AddSingleton<IFeatureReader, FeatureStore>();

            // DI031: a second descriptor means a second FeatureStore. Writes through
            // IFeatureWriter are invisible to IFeatureReader.
            services.AddSingleton<IFeatureWriter, FeatureStore>();
        }

Sample app forwarded-registration pattern

        public static void ConfigureGood(IServiceCollection services)
        {
            services.AddSingleton<FeatureStore>();
            services.AddSingleton<IFeatureReader>(sp => sp.GetRequiredService<FeatureStore>());
            services.AddSingleton<IFeatureWriter>(sp => sp.GetRequiredService<FeatureStore>());
        }

Related guides

  • No problem-guide pages point here yet.

Nearby diagnostics

Other rules in this family

All 37 rules