.NET  

How to implement dependency injection in .NET the right way?

Introduction

Dependency Injection (DI) is one of the most important patterns in modern .NET development. It helps you write cleaner, maintainable, testable, and scalable applications by removing tight coupling between classes. Instead of a class creating its own dependencies, DI allows you to “inject” those dependencies from the outside. This approach improves flexibility and makes your code easier to test and replace. In this article, we’ll break down DI in simple words and show you the correct way to implement it in .NET using practical examples.

What Is Dependency Injection?

Dependency Injection means giving an object the things it needs (dependencies) from the outside instead of letting it create them.

Bad Approach (Tightly Coupled)

public class OrderService {
    private readonly EmailService _email;

    public OrderService() {
        _email = new EmailService();
    }
}

Here, OrderService creates its own dependency, making it hard to test or replace.

Good Approach (Using DI)

public class OrderService {
    private readonly IEmailService _email;

    public OrderService(IEmailService email) {
        _email = email;
    }
}

The dependency is injected—much cleaner and more flexible.

Why Dependency Injection Is Important

  • Makes classes loosely coupled

  • Improves testability (mock services easily)

  • Encourages cleaner architecture

  • Makes it easier to replace implementations

  • Built-in DI container in .NET reduces extra configuration

DI is a core part of Clean Architecture, Onion Architecture, and Domain-Driven Design.

Registering Services in .NET

In .NET, DI is configured in Program.cs using the built-in service container.

Example Registration

builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<ILoggerService, LoggerService>();

Each registration defines:

  • InterfaceIEmailService

  • ImplementationEmailService

Understanding Service Lifetimes

Choosing the correct service lifetime is important.

1. Transient

A new instance is created every time it is requested.

services.AddTransient<IEmailService, EmailService>();

Use when: lightweight, stateless services.

2. Scoped

Created once per HTTP request.

services.AddScoped<IOrderService, OrderService>();

Use when: services dealing with per-request operations such as database operations.

3. Singleton

Created only once for the entire application.

services.AddSingleton<ILoggerService, LoggerService>();

Use when: shared data, configuration loaders, loggers.

Choosing the Right Lifetime

  • Transient → frequent, lightweight tasks

  • Scoped → web request-specific operations

  • Singleton → share the same instance safely

Using Constructor Injection (Best Practice)

Constructor injection is the most common and recommended way to inject dependencies.

Example

public class OrderController : ControllerBase {
    private readonly IOrderService _orderService;

    public OrderController(IOrderService orderService) {
        _orderService = orderService;
    }

    [HttpGet]
    public IActionResult GetOrders() => Ok(_orderService.GetOrders());
}

Why Constructor Injection Is Best

  • Clear required dependencies

  • Works with unit testing

  • Ensures class cannot exist without its dependencies

Using Interface-Based Design

Always create interfaces for services.

Example

public interface IEmailService {
    void Send(string to, string message);
}

public class EmailService : IEmailService {
    public void Send(string to, string message) {}
}

Benefits

  • Easy to swap implementations

  • Perfect for unit testing

  • Encourages loose coupling

Avoiding Service Locator Anti-pattern

Wrong Approach

var emailService = HttpContext.RequestServices.GetService<IEmailService>();

This is called Service Locator and should be avoided because:

  • Hard to test

  • Hides dependencies

  • Breaks dependency inversion

Always inject dependencies instead.

Registering Generic Services

You can register generic types too.

Example

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

Useful in repository patterns.

Using DI in Background Services

Hosted services also support DI.

Example

public class EmailBackgroundService : BackgroundService {
    private readonly IEmailService _emailService;

    public EmailBackgroundService(IEmailService emailService) {
        _emailService = emailService;
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken) {
        // Logic here
        return Task.CompletedTask;
    }
}

Register it

services.AddHostedService<EmailBackgroundService>();

Using Options Pattern with DI

Configuration settings should also be injected.

Example

services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));

Inject it

public class EmailService : IEmailService {
    private readonly EmailSettings _settings;

    public EmailService(IOptions<EmailSettings> options) {
        _settings = options.Value;
    }
}

Common Mistakes to Avoid

  • Registering the wrong lifetime (e.g., using Singleton for DbContext)

  • Injecting too many services into one class → violates Single Responsibility Principle

  • Using service locator instead of constructor injection

  • Forgetting to register an interface

  • Circular dependencies between services

Best Practices for Dependency Injection in .NET

  • Prefer constructor injection

  • Use interfaces for all services

  • Group service registrations in extension methods

  • Choose correct service lifetimes

  • Keep services small and single-purpose

  • Avoid building dependency graphs manually

  • Use the built-in DI container unless advanced features are needed

Conclusion

Dependency Injection is one of the most powerful tools for building scalable and maintainable .NET applications. By understanding how DI works, choosing the right lifetimes, using constructor injection, and avoiding common pitfalls, you can write clean, testable, and well-structured code. Implementing DI the right way leads to better architecture, easier unit testing, and long-term maintainability for your .NET projects.