.NET  

How to implement Dependency Injection in .NET

Dependency Injection (DI) is a core concept in .NET since .NET Core, and it's the default pattern used in ASP.NET Core apps, console apps, and even workers or background services.

Whether you're building a web API or a desktop tool, mastering DI leads to cleaner, testable, and maintainable code.

1. What is Dependency Injection?

Dependency Injection is a design pattern where.

  • A class doesn't create its dependencies.
  • Instead, they are "injected" from outside (via constructors or setters).

This follows Inversion of Control (IoC), where the control of creating objects is inverted and handled by a container.

Without DI

public class ReportService
{
    private readonly EmailSender _emailSender = new EmailSender(); // tightly coupled
}

With DI

public class ReportService
{
    private readonly IEmailSender _emailSender;
    public ReportService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }
}

2. Setting Up Dependency Injection in .NET

DI in .NET is handled by the built-in service container via IServiceCollection.

In a .NET Core or .NET 6+ app, services are registered in Program.cs.

Example

var builder = WebApplication.CreateBuilder(args);

// Registering services
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddScoped<IReportService, ReportService>();

var app = builder.Build();

app.MapGet("/", (IReportService reportService) =>
{
    reportService.GenerateReport();
    return "Report Generated!";
});
app.Run();

3. Types of Dependency Injection

Constructor Injection (most common)

public class ReportService
{
    private readonly IEmailSender _emailSender;
    public ReportService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }
}

Method Injection

public class NotificationService
{
    public void Notify(IEmailSender sender)
    {
        sender.Send("Hello!");
    }
}

Property Injection (less preferred)

public class OrderService
{
    public IEmailSender EmailSender { get; set; }
}

4. Service Lifetimes Explained

When registering services, choose an appropriate lifetime.

Lifetime Description Example
Transient New instance every time AddTransient
Scoped One per request (web/API) AddScoped
Singleton One instance for the app AddSingleton
builder.Services.AddTransient<IService, MyService>();        // lightweight
builder.Services.AddScoped<IRepository, DbRepository>();     // web request
builder.Services.AddSingleton<ILogger, MyLogger>();          // config or caching

5. Injecting Services into Controllers or Endpoints

Example: ASP.NET Core Web API

[ApiController]
[Route("[controller]")]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService;
    public OrdersController(IOrderService orderService)
    {
        _orderService = orderService;
    }
    [HttpGet]
    public IActionResult GetOrders()
    {
        var orders = _orderService.GetAll();
        return Ok(orders);
    }
}

6. Unit Testing with Dependency Injection

Mock dependencies using interfaces for testing.

var mockSender = new Mock<IEmailSender>();
mockSender.Setup(s => s.Send(It.IsAny<string>()));

var reportService = new ReportService(mockSender.Object);
reportService.GenerateReport();

DI makes testing cleaner and avoids hard-coded dependencies.

7. Best Practices

  • Always program to interfaces, not implementations.
  • Keep service lifetimes in sync with their dependencies.
  • Avoid the ServiceLocator anti-pattern.
  • Don't overuse a singleton for services using HttpContext or DB connections.
  • Group service registrations using extension methods.
    public static class ServiceCollectionExtensions
    {
        public static void AddMyAppServices(this IServiceCollection services)
        {
            services.AddScoped<IReportService, ReportService>();
            services.AddTransient<IEmailSender, EmailSender>();
        }
    }
    

8. Common DI Errors

Error Cause Fix
Cannot resolve service Missing registration Add builder.Services.AddX
Lifetime mismatch Using a singleton inside a scope Change lifetime or use factory
Null reference Optional dependencies not injected Use required services only

Summary

.NET’s built-in dependency injection is fast, clean, and customizable.

By using DI,

  • Your code becomes decoupled
  • Testing becomes easier
  • Maintenance becomes manageable

Start with simple constructor injection, register lifetimes properly, and evolve your architecture as needed.