/**/

Dependency Injection in .NET: The Advanced Features You're Ignoring

3 min readAjay Patel

If you write .NET code, you use the built-in Microsoft.Extensions.DependencyInjection container. It’s unavoidable.

Usually, the interaction goes exactly like this: you create an interface, you create an implementation, and you dump it in Program.cs with builder.Services.AddScoped<IMyService, MyService>();. Job done.

But over the last few years, Microsoft has quietly added some incredibly powerful features to the native DI container. Features that used to require massive third-party libraries like Autofac or Ninject are now built right in.

If you are still just blindly registering singletons, you are missing out. Here are the DI features you should be using.

1. Keyed Services (Finally!)

For years, if you had two implementations of the same interface (e.g., IPaymentGateway implemented by StripeGateway and PayPalGateway), the native DI container struggled. If you registered both, requesting IPaymentGateway would just give you whichever one you registered last.

We used to solve this with ugly factory patterns: Func<string, IPaymentGateway>.

As of .NET 8, Keyed Services are natively supported. You can register multiple implementations of the same interface, tagged with a key:

// Register with a key
builder.Services.AddKeyedScoped<IPaymentGateway, StripeGateway>("stripe");
builder.Services.AddKeyedScoped<IPaymentGateway, PayPalGateway>("paypal");

When you need to inject them, you just use the [FromKeyedServices] attribute:

public class CheckoutService(
    [FromKeyedServices("stripe")] IPaymentGateway stripe,
    [FromKeyedServices("paypal")] IPaymentGateway paypal)
{
    public void Pay(string method)
    {
        if (method == "stripe") stripe.Process();
        // ...
    }
}

This single feature allowed me to rip out hundreds of lines of boilerplate factory classes from my last project.

2. The AddHostedService Trap

Background tasks are great. You inject an IHostedService, it runs in the background, life is good.

But what happens if you need to use a Scoped service (like your EF Core ApplicationDbContext) inside your background task?

// 🚨 This will crash on startup
public class MyBackgroundWorker(ApplicationDbContext db) : BackgroundService 
{
    // ...
}

Hosted Services are Singletons. If you try to inject a Scoped service into a Singleton, the DI container will throw a massive exception at startup.

The Fix: You must inject IServiceProvider and manually create a scope when the background task runs.

public class MyBackgroundWorker(IServiceProvider services) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // ✅ Correct way to use Scoped services in a Background worker
        using var scope = services.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        
        await db.Users.AddAsync(new User { Name = "Test" });
        await db.SaveChangesAsync();
    }
}

3. TryAdd vs Add

If you are writing a class library or a NuGet package, you should almost never use .AddScoped(). You should be using .TryAddScoped() from the Microsoft.Extensions.DependencyInjection.Extensions namespace.

When an application registers services, they might want to override your library's default implementation.

// In your library:
services.TryAddScoped<IEmailSender, DefaultEmailSender>();

// In the consuming app's Program.cs:
builder.Services.AddScoped<IEmailSender, CustomSendGridSender>();
builder.Services.AddMyLibrary(); 

Because you used TryAdd, the DI container will see that the app already registered IEmailSender, and it will politely skip registering your DefaultEmailSender. If you used Add, you would overwrite the app's custom implementation, leading to incredibly frustrating debugging sessions for the developers using your library.

The Takeaway

The native .NET DI container is no longer a "lightweight" compromise. It is a fully-featured, high-performance beast. Use Keyed Services to clean up your factories, be careful with scopes in background tasks, and play nice with TryAdd if you are writing shared code.

About the Author

Ajay Patel

Software Developer specializing in .NET, C#, and ASP.NET Core. Built .NET Toolbox to give developers privacy-first, browser-based utilities for their daily workflow.

Read more about Ajay