Dependency Injection in .NET: Explained with Real Use Cases

VaibhavVaibhav
4 min read

Introduction

Dependency Injection (DI) is one of the core concepts that makes modern .NET development powerful, testable, and clean. But often, developers either overcomplicate it or underutilize its true potential.

In this article, I’ll walk you through what DI is, how it works in .NET (especially .NET 6 and above), and most importantly real-world use cases where I’ve implemented DI effectively in enterprise apps.


What is Dependency Injection?

Dependency Injection is a design pattern used to implement Inversion of Control (IoC) between classes and their dependencies. Instead of a class instantiating its dependencies directly, they are provided externally—usually by the framework.

Benefits:

  • Better code maintainability

  • Enhanced testability

  • Promotes loose coupling


DI in .NET Core and .NET 8

.NET has built-in support for DI out of the box.

Typical usage in Program.cs:

var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<ICacheManager, MemoryCacheManager>();
builder.Services.AddTransient<IEmailSender, EmailSender>();

var app = builder.Build();

Real Use Case 1: Service Layer Abstractions

public interface IOrderService
{
    void ProcessOrder(Order order);
}

public class OrderService : IOrderService
{
    private readonly IEmailSender _emailSender;

    public OrderService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public void ProcessOrder(Order order)
    {
        _emailSender.Send(order.CustomerEmail, "Order received!");
    }
}

Use Case 2: Unit Testing with Mocks

var mockEmailSender = new Mock<IEmailSender>();
var orderService = new OrderService(mockEmailSender.Object);

orderService.ProcessOrder(new Order { CustomerEmail = "test@example.com" });

mockEmailSender.Verify(x => x.Send(It.IsAny<string>(), It.IsAny<string>()), Times.Once);

Use Case 3: Replace Service Implementations Dynamically

#if DEBUG
builder.Services.AddSingleton<IPaymentGateway, MockPaymentGateway>();
#else
builder.Services.AddSingleton<IPaymentGateway, StripePaymentGateway>();
#endif

Use Case 4: Scoped Dependencies in Web APIs

builder.Services.AddScoped<IRequestContext, RequestContext>();

Useful for keeping request-specific data like current user or tenant ID.


Use Case 5: Middleware and DI

public class AuditMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IAuditService _auditService;

    public AuditMiddleware(RequestDelegate next, IAuditService auditService)
    {
        _next = next;
        _auditService = auditService;
    }

    public async Task Invoke(HttpContext context)
    {
        _auditService.Track(context);
        await _next(context);
    }
}

Register it:

app.UseMiddleware<AuditMiddleware>();

Understanding Service Lifetimes in .NET Core DI

1. Transient

services.AddTransient<IMyService, MyService>();
  • New instance every time

  • Use for lightweight, stateless services

2. Scoped

services.AddScoped<IMyService, MyService>();
  • One instance per request

  • Ideal for request-specific services

3. Singleton

services.AddSingleton<IMyService, MyService>();
  • Single instance for the app lifetime

  • Use cautiously, ensure thread safety


Best Practices for Using Dependency Injection

  1. Depend on Abstractions: Use interfaces instead of concrete classes.

  2. Keep Constructors Light: Avoid over-injecting; use Facade if needed.

  3. Avoid Service Locator: Don’t inject IServiceProvider to resolve dependencies manually.

  4. Dispose with Care: Let the container manage IDisposable lifetimes.

  5. Use TryAdd for Libraries: Prevent overriding user services.

  6. Use IOptions<T> for Configs: Avoid injecting configuration directly.


Reflection and Dependency Injection

What is Reflection?

Reflection allows inspecting types and metadata at runtime and is used internally by .NET DI to:

  • Discover constructors

  • Resolve dependencies

  • Instantiate services

Manual Use:

var instance = ActivatorUtilities.CreateInstance(serviceProvider, typeof(MyService));

Use in frameworks or when dynamic registration is necessary.


Implementing DI Using Reflection with Marker Interfaces

Define marker interfaces:

public interface IScoped { }
public interface ISingleton { }
public interface ITransient { }

Use them in service classes:

public class OrderService : IOrderService, IScoped { }

Automated registration:

var types = AppDomain.CurrentDomain.GetAssemblies()
    .SelectMany(a => a.GetTypes())
    .Where(t => t.IsClass && !t.IsAbstract);

foreach (var type in types)
{
    var interfaces = type.GetInterfaces();

    if (interfaces.Contains(typeof(IScoped)))
        services.AddScoped(interfaces.First(i => i != typeof(IScoped)), type);
    else if (interfaces.Contains(typeof(ISingleton)))
        services.AddSingleton(interfaces.First(i => i != typeof(ISingleton)), type);
    else if (interfaces.Contains(typeof(ITransient)))
        services.AddTransient(interfaces.First(i => i != typeof(ITransient)), type);
}

This avoids registering each dependency manually.


Using DI for Multiple Inheritance (Multiple Interface Injection)

.NET doesn’t support multiple class inheritance but allows implementing multiple interfaces:

public interface IAuditService { void Track(); }
public interface ILoggingService { void Log(); }

public class AuditLogger : IAuditService, ILoggingService
{
    public void Track() { }
    public void Log() { }
}

Registering both interfaces:

services.AddScoped<IAuditService, AuditLogger>();
services.AddScoped<ILoggingService>(sp => (ILoggingService)sp.GetRequiredService<IAuditService>());

This allows both interfaces to resolve to the same instance.


Conclusion

Dependency Injection is not just a buzzword, it's a foundational principle that simplifies architecture, improves testability, and enforces separation of concerns. Whether you're building microservices or monoliths, DI will help you maintain cleaner, more manageable code.

Start small, follow best practices, and gradually scale your DI strategy with reflection, marker interfaces, and multiple interface mapping.

Happy coding!

0
Subscribe to my newsletter

Read articles from Vaibhav directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Vaibhav
Vaibhav

I break down complex software concepts into actionable blog posts. Writing about cloud, code, architecture, and everything in between.