Thumbnail for Dependency Injection and IoC in ASP.NET Core: A Professional Guide

Dependency Injection and IoC in ASP.NET Core: A Professional Guide

Published: 2025-07-29

Dependency Injection and IoC in ASP.NET Core: A Professional Guide

In the world of professional .NET application development, Dependency Injection (DI) and Inversion of Control (IoC) are not just trendy buzzwords, but fundamental concepts that impact the quality, testability, and scalability of our code. This post focuses on the practical use of built-in DI in ASP.NET Core.

🧩 Dependency Injection (DI) / Inversion of Control (IoC)

What is Dependency Injection?

Dependency Injection is a technique where dependencies (such as services, repositories, loggers) are not created within a class, but are supplied from the outside – most commonly through the constructor. This approach increases code flexibility and testability.

public class OrderService
{
    private readonly IEmailSender _emailSender;

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

    public void ProcessOrder(Order order)
    {
        // order processing logic
        _emailSender.Send(order.CustomerEmail);
    }
}

What is Inversion of Control?

Inversion of Control (IoC) is a general concept where we – or rather: the DI container – take control over object creation and their lifecycle. This is an inversion of the traditional programming approach, where objects created their own dependencies.

🎯 Benefits of Using Dependency Injection

  1. Loose Coupling – components are not dependent on concrete implementations
  2. Better Modularity – components are more autonomous and reusable
  3. Easy Testing – dependencies can be easily replaced with mocks during unit tests
  4. Readability and Maintainability – responsibility for dependencies is explicit and easy to manage

🛠 Built-in DI Container in ASP.NET Core

ASP.NET Core offers a built-in and lightweight DI container: Microsoft.Extensions.DependencyInjection

It supports three main dependency lifecycles:

Lifecycle Description
Singleton One instance for the entire application lifetime
Scoped One instance per HTTP request
Transient New instance with every dependency request

📌 Dependency Registration – Examples

services.AddSingleton<IMyService, MyService>();     // one object for entire application
services.AddScoped<IRepository, Repository>();      // one object per HTTP request
services.AddTransient<IHandler, Handler>();         // new object every time

🧠 Best Practices

  • Interface-based design principle – base dependencies on abstractions, not concrete classes
  • Avoid service locator pattern – manually resolving dependencies through IServiceProvider.GetService() is an anti-pattern
  • Group dependencies with modularity – divide service registration into modules (e.g., AddDomainServices, AddInfrastructureServices)
  • Consider using more advanced containers like Autofac if your project requires advanced dependency management

🧩 DI and Design Patterns

DI works excellently with classic design patterns:

  • Factory – decides how to create a dependency (e.g., with complex configuration processes)
  • Strategy – dynamic behavior substitution through registered implementations
  • Decorator – easy injection of layers decorating functionality (e.g., caching, logging)
  • Mediator – elimination of direct dependencies between components, handler registration

❌ What NOT to Do

Service Locator Anti-Pattern

// DON'T DO THIS
public class BadOrderService
{
    private readonly IServiceProvider _serviceProvider;
    
    public BadOrderService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public void ProcessOrder(Order order)
    {
        var emailSender = _serviceProvider.GetService<IEmailSender>();
        emailSender.Send(order.CustomerEmail);
    }
}

Circular Dependencies

// DON'T CREATE CIRCULAR DEPENDENCIES
public class ServiceA
{
    public ServiceA(ServiceB serviceB) { }
}

public class ServiceB
{
    public ServiceB(ServiceA serviceA) { } // This creates a circular dependency!
}

Registering Concrete Classes Instead of Interfaces

// AVOID THIS - reduces flexibility
services.AddScoped<EmailSender>(); // concrete class

// DO THIS INSTEAD
services.AddScoped<IEmailSender, EmailSender>(); // interface-based

Mixing Lifecycles Incorrectly

// DON'T inject shorter-lived services into longer-lived ones
services.AddSingleton<ISingletonService, SingletonService>(); // lives for entire app
services.AddScoped<IScopedService, ScopedService>(); // lives per request

// BAD: Singleton depending on Scoped service
public class SingletonService : ISingletonService
{
    public SingletonService(IScopedService scopedService) // This can cause issues!
    {
    }
}

Constructor Over-Injection

// DON'T create constructors with too many dependencies
public class OverloadedService
{
    // Too many dependencies - consider refactoring
    public OverloadedService(
        IService1 service1,
        IService2 service2,
        IService3 service3,
        IService4 service4,
        IService5 service5,
        IService6 service6,
        IService7 service7) // This suggests the class has too many responsibilities
    {
    }
}

Not Disposing Resources Properly

// DON'T manually dispose DI-managed objects
public class BadService
{
    public void DoWork(IDisposableService service)
    {
        service.DoSomething();
        service.Dispose(); // DON'T - let the DI container handle disposal
    }
}

Summary

Dependency Injection is not just a convenient tool, but an architectural foundation of modern .NET applications. When used well – combined with thoughtful design and patterns – it leads to code that is clear, scalable, and testable.

Back to Blog