The Real Difference Between Add, TryAdd, and TryAddEnumerable in .NET

A lot of developers think DI registration is simple:

services.AddScoped<IMyService, MyService>();

Done.

Until someone registers the same interface twice.
Or a NuGet package adds a default implementation.
Or your IEnumerable<T> suddenly contains duplicates.

Let’s walk through this properly.

1️⃣ What Add* Actually Does

When you call:

services.AddScoped<IProcessor, ProcessorA>();
services.AddScoped<IProcessor, ProcessorB>();

You are not replacing the previous registration.

You are adding another descriptor to the container.

The container internally stores something like:

IProcessor → ProcessorA
IProcessor → ProcessorB

Now resolution depends on how you inject.

Injecting a Single Instance

public class Handler
{
    public Handler(IProcessor processor)
    {
    }
}

You get:

ProcessorB (the last registration)

Last registration wins.

Injecting IEnumerable

public class Handler
{
    public Handler(IEnumerable<IProcessor> processors)
    {
    }
}

You get:

ProcessorA
ProcessorB

In registration order.

This behavior is fundamental.
It explains 90% of DI confusion.

2️⃣ TryAdd — Conditional Registration

TryAdd exists for one reason:

Register this service only if it hasn’t been registered before.

Example:

services.TryAddScoped<IEmailSender, DefaultEmailSender>();

If IEmailSender is already registered (even once), this does nothing.
It checks:

Is there ANY ServiceDescriptor with this ServiceType?

If yes → skip.

Why This Exists

Because libraries shouldn’t force implementations.

Imagine you write a logging package:

services.TryAddSingleton<ILogger, ConsoleLogger>();

Now the app developer can override it:

services.AddSingleton<ILogger, SerilogLogger>();

If your library used AddSingleton instead of TryAddSingleton,
you’d override the application’s choice.
That’s bad library design.
TryAdd makes your package extensible by default.

3️⃣ TryAddEnumerable — The One Most People Misunderstand

This one is different.

It doesn’t check whether the service type exists.

It checks whether the same implementation type already exists.

Example:

services.TryAddEnumerable(
    ServiceDescriptor.Scoped<IValidator, EmailValidator>());

If EmailValidator is already registered for IValidator,
it will NOT be added again.

But this still works:

services.TryAddEnumerable(
    ServiceDescriptor.Scoped<IValidator, PhoneValidator>());

Because it’s a different implementation.

Why Not Use TryAdd?

Because TryAdd would block ALL additional registrations.

Example:

services.TryAddScoped<IValidator, EmailValidator>();
services.TryAddScoped<IValidator, PhoneValidator>();

Only the first one is added.

That’s not what you want when building pipelines.

Real Use Case: Handler Pipelines

Think about:

  • INotificationHandler
  • IMiddleware
  • IPipelineBehavior
  • IValidator

You want multiple implementations.

But you don’t want duplicates if two modules register the same one.

That’s exactly what TryAddEnumerable solves.

4️⃣ Replace — Overwrite Intentionally

Sometimes you don’t want “last wins”.

You want explicit replacement.

services.Replace(
    ServiceDescriptor.Scoped<IEmailSender, NewEmailSender>());

This:

  • Removes existing registrations for IEmailSender
  • Adds the new one

It’s cleaner than stacking registrations.

Useful in:

  • Integration testing
  • Swapping real services with mocks
  • Overriding framework services

5️⃣ RemoveAll — Nuclear Option

services.RemoveAll<IEmailSender>();

Removes all registrations for that service type.

Often used in test setups:

services.RemoveAll<IEmailSender>();
services.AddScoped<IEmailSender, FakeEmailSender>();

Clean override.

6️⃣ Subtle but Important: Registration Order Matters

The container preserves registration order.

This matters when:

  • Injecting IEnumerable<T>
  • Building middleware pipelines
  • Using decorators manually

Example:

services.AddScoped<IProcessor, LoggingProcessor>();
services.AddScoped<IProcessor, ValidationProcessor>();
services.AddScoped<IProcessor, BusinessProcessor>();

IEnumerable<IProcessor> resolves in this order:

  1. Logging
  2. Validation
  3. Business

If you change the order, behavior changes.
There is no sorting. No magic.
Just insertion order.

7️⃣ Edge Case: Open Generics

All these methods work with open generics too:

services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));

TryAdd works the same way.

But remember:
TryAdd checks by ServiceType —
so registering different generic constraints can behave unexpectedly.

Be careful in shared libraries.

8️⃣ What Most Developers Don’t Realize

The container stores registrations as a simple list:

List<ServiceDescriptor>

Resolution rules are simple:

  • Single injection → last descriptor
  • IEnumerable injection → all descriptors (in order)
  • TryAdd → check existence by ServiceType
  • TryAddEnumerable → check existence by ServiceType + ImplementationType

There’s no complex resolution tree.
No ranking system.
No override priority.

It’s deterministic. Predictable. Simple.

And that’s why understanding it matters.

Quick Comparison Table

Method Allows Multiple? Skips If Exists? Prevents Duplicates? Typical Use
Add Yes No No Normal registration
TryAdd No (first wins) Yes (by ServiceType) Yes Library defaults
TryAddEnumerable Yes Yes (by Impl type) Yes (per impl) Pipelines
Replace No Removes existing Yes Explicit override
RemoveAll No Clears all Yes Testing / resets

Practical Rules I Follow

  • Application code → use Add
  • Reusable libraries → use TryAdd
  • Handler collections → use TryAddEnumerable
  • Tests → use RemoveAll + Add
  • Intentional override → use Replace

Keep it explicit.

Final Thought

Most DI confusion doesn’t come from lifetimes.

It comes from misunderstanding registration behavior.

When something “randomly” resolves the wrong implementation,
it’s almost always because:

  • You registered twice
  • Order changed
  • Or TryAdd silently skipped something

The container isn’t being clever.

It’s being literal.

And once you understand that,
you stop guessing — and start designing your registrations intentionally.

Leave a Reply