DEV Community

Vimal
Vimal

Posted on

Dependency Injection in .NET Core

Overview

Dependency Injection (DI) is a software design pattern and technique that deals with how components acquire their dependencies. Rather than creating dependencies internally, a class receives them from the outside — improving modularity, testability, and flexibility.

In .NET, Dependency Injection is a first-class citizen, built directly into the ASP.NET Core framework.


Purpose

Dependency Injection aims to separate the creation of a component’s dependencies from the component’s behavior, promoting Inversion of Control (IoC). The class should focus only on what it needs to do, not on how to create the things it needs.

"Don't call me, I’ll call you." — This summarizes Inversion of Control, where the framework injects required services into your class.

Some of the common dependencies needed in application are:

  • Services (ILogger, DbContext, etc.)
  • Repositories (ICustomerRepository)
  • Configurations (IOptions)
  • External integrations (IEmailService, IPaymentGateway)

Types of Dependency Injection

There are three common forms of DI:

  1. Constructor Injection (most common)
  2. Setter Injection (property-based)
  3. Interface Injection (less common in .NET)

1. Constructor Injection

The dependency is provided through the class constructor.

public interface IMessageService
{
    string GetMessage();
}

public class EmailMessageService : IMessageService
{
    public string GetMessage() => "Message via Email";
}

public class NotificationManager
{
    private readonly IMessageService _messageService;

    public NotificationManager(IMessageService messageService)
    {
        _messageService = messageService;
    }

    public void Notify()
    {
        Console.WriteLine(_messageService.GetMessage());
    }
}
Enter fullscreen mode Exit fullscreen mode

Registration in .NET Core

builder.Services.AddTransient<IMessageService, EmailMessageService>();
builder.Services.AddTransient<NotificationManager>();
Enter fullscreen mode Exit fullscreen mode

2. Setter (Property) Injection

Dependencies are set via public properties. Not natively supported by the built-in container, but possible manually.

public class LoggerService
{
    public IMessageService MessageService { get; set; }

    public void Log()
    {
        Console.WriteLine(MessageService?.GetMessage());
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, MessageService would need to be set manually after object construction.


3. Interface Injection

The dependency provides an interface to inject itself into the consumer. Rare in .NET:

public interface IInjectable
{
    void Inject(IMessageService messageService);
}
Enter fullscreen mode Exit fullscreen mode

Consumers then implement Inject to receive the dependency.


Lifetimes in .NET DI

When registering services, you specify how long a service lives:

  • Transient – new instance each time
  • Scoped – one instance per request
  • Singleton – one instance for the app lifetime

builder.Services.AddTransient<IMyService, MyService>(); // new each time
builder.Services.AddScoped<IMyService, MyService>();    // one per web request
builder.Services.AddSingleton<IMyService, MyService>(); // shared instance

Enter fullscreen mode Exit fullscreen mode

Dependency Graph

.NET Core builds a dependency graph during application startup. Circular dependencies (A → B → A) are automatically detected and cause an error.


Benefits of DI

  • Decoupling: Easier to replace or extend components
  • Testability: Easier to write unit tests using mocks
  • Reusability: Services can be reused across classes
  • Maintainability: Easier to manage and evolve large applications

Real-world Example

public interface IPaymentProcessor
{
    void Process(decimal amount);
}

public class StripePaymentProcessor : IPaymentProcessor
{
    public void Process(decimal amount)
    {
        Console.WriteLine($"Processed ${amount} via Stripe.");
    }
}

public class CheckoutService
{
    private readonly IPaymentProcessor _paymentProcessor;

    public CheckoutService(IPaymentProcessor paymentProcessor)
    {
        _paymentProcessor = paymentProcessor;
    }

    public void Checkout()
    {
        _paymentProcessor.Process(100);
    }
}
Enter fullscreen mode Exit fullscreen mode

Registration:

builder.Services.AddScoped<IPaymentProcessor, StripePaymentProcessor>();
builder.Services.AddScoped<CheckoutService>();
Enter fullscreen mode Exit fullscreen mode

Usage:

var checkout = app.Services.GetRequiredService<CheckoutService>();
checkout.Checkout(); // Outputs: Processed $100 via Stripe.
Enter fullscreen mode Exit fullscreen mode

Anti-Patterns to Avoid

  • Service Locator Pattern: Using IServiceProvider.GetService() everywhere breaks DI principles.
  • Too Many Dependencies: If a class has too many injected services, it's likely violating Single Responsibility Principle.
  • Mixing lifetimes improperly: Injecting Scoped into Singleton can cause unexpected behavior.

Conclusion

Dependency Injection in .NET Core encourages clean architecture by separating behavior from object creation. It's built-in, powerful, and essential for test-driven and scalable .NET applications.

By focusing on what your classes need to do, and not how they get their dependencies, your application becomes more maintainable, testable, and adaptable to change.

Top comments (0)