C# Dependency Injection Explained: Build Loosely Coupled Applications

Dependency injection is a common design technique in modern C# and .NET applications. It allows classes to receive the services they depend on instead of creating those services directly.

What is Dependency Injection?

Dependency injection, often called DI, is a technique where an object receives its required dependencies from an external source. This reduces tight coupling and makes components easier to replace and test.

CSHARP
interface IMessageSender
{
    void Send(string message);
}

class EmailSender : IMessageSender
{
    public void Send(string message)
    {
        Console.WriteLine($"Email: {message}");
    }
}

class NotificationService
{
    private readonly IMessageSender sender;

    public NotificationService(IMessageSender sender)
    {
        this.sender = sender;
    }

    public void Notify(string message)
    {
        sender.Send(message);
    }
}

Why Use Dependency Injection?

  • Reduce tight coupling between classes
  • Make implementations easier to replace
  • Improve unit testing
  • Centralize service configuration
  • Support cleaner application architecture
  • Make large applications easier to maintain

Without Dependency Injection

Without DI, a class may create its dependencies directly. This makes the class responsible for both its own behavior and dependency construction.

CSHARP
class NotificationService
{
    private readonly EmailSender sender = new EmailSender();

    public void Notify(string message)
    {
        sender.Send(message);
    }
}

Constructor Injection

Constructor injection is the most common form of dependency injection in C#. Dependencies are provided through the class constructor.

CSHARP
class NotificationService
{
    private readonly IMessageSender sender;

    public NotificationService(IMessageSender sender)
    {
        this.sender = sender;
    }

    public void Notify(string message)
    {
        sender.Send(message);
    }
}

Interface-Based Dependency Injection

Using interfaces allows an application to depend on an abstraction rather than a specific implementation.

CSHARP
interface IPaymentService
{
    void Process(decimal amount);
}

class CardPaymentService : IPaymentService
{
    public void Process(decimal amount)
    {
        Console.WriteLine($"Card payment: {amount}");
    }
}

class OrderService
{
    private readonly IPaymentService paymentService;

    public OrderService(IPaymentService paymentService)
    {
        this.paymentService = paymentService;
    }
}

Dependency Injection in .NET

Modern .NET applications commonly use the built-in dependency injection container. Services can be registered with the application's service collection and automatically supplied to constructors.

CSHARP
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMessageSender, EmailSender>();
builder.Services.AddScoped<NotificationService>();

var app = builder.Build();

Service Registration

The service collection provides registration methods that describe how a dependency should be created and managed.

CSHARP
builder.Services.AddTransient<IEmailService, EmailService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddSingleton<ICacheService, CacheService>();

Transient Lifetime

Transient services are created each time the dependency is requested. They are commonly suitable for lightweight, stateless services.

CSHARP
builder.Services.AddTransient<IMessageFormatter, MessageFormatter>();

Scoped Lifetime

Scoped services are created once per scope. In ASP.NET Core applications, a request commonly represents a scope.

CSHARP
builder.Services.AddScoped<IOrderService, OrderService>();

Singleton Lifetime

Singleton services use one instance for the lifetime of the application or service provider.

CSHARP
builder.Services.AddSingleton<IApplicationCache, ApplicationCache>();

Service Lifetime Comparison

LifetimeInstance CreationTypical Use
TransientNew instance each time requestedLightweight stateless services
ScopedOne instance per scopeRequest-based application services
SingletonOne instance for the service provider lifetimeShared application-wide services

Multiple Dependencies

A class can receive multiple dependencies through its constructor. Each dependency can represent a separate responsibility.

CSHARP
class OrderService
{
    private readonly IPaymentService payment;
    private readonly INotificationService notification;
    private readonly ILogger<OrderService> logger;

    public OrderService(
        IPaymentService payment,
        INotificationService notification,
        ILogger<OrderService> logger)
    {
        this.payment = payment;
        this.notification = notification;
        this.logger = logger;
    }
}

Dependency Injection and Testing

DI makes unit testing easier because a real dependency can be replaced with a fake or mock implementation.

CSHARP
class FakeMessageSender : IMessageSender
{
    public string? LastMessage { get; private set; }

    public void Send(string message)
    {
        LastMessage = message;
    }
}

var fakeSender = new FakeMessageSender();
var service = new NotificationService(fakeSender);

service.Notify("Test message");

Console.WriteLine(fakeSender.LastMessage);

DI and Configuration

Dependency injection can also be used to provide configuration, logging, database services, HTTP clients, and other infrastructure components.

CSHARP
builder.Services.AddHttpClient<WeatherService>();
builder.Services.AddLogging();
builder.Services.AddOptions();

Dependency Injection vs Direct Creation

ApproachDependency CreationFlexibility
Direct creationClass creates dependencyLower
Constructor injectionExternal container or caller provides dependencyHigh
Interface-based DIImplementation can be replacedVery high

Common DI Mistakes

  • Registering services with the wrong lifetime
  • Creating dependencies manually inside DI-managed classes
  • Injecting too many unrelated dependencies
  • Using singleton services with unsafe shared mutable state
  • Depending on concrete implementations unnecessarily
  • Treating DI as a replacement for good architecture

Dependency Injection Best Practices

  • Prefer constructor injection for required dependencies
  • Depend on abstractions when replacement is useful
  • Choose service lifetimes carefully
  • Keep services focused on clear responsibilities
  • Avoid excessively large constructors
  • Keep dependency registration centralized and understandable

Real-World Applications

  • ASP.NET Core Web APIs
  • Database repositories
  • Payment services
  • Email and notification systems
  • Logging infrastructure
  • External API clients
  • Caching services

Practice Exercises

  • Create an ILogger interface and implementation
  • Inject a service into another class
  • Register services using AddTransient
  • Create a scoped application service
  • Build a singleton cache service
  • Replace a real dependency with a fake implementation
  • Create a small ASP.NET Core application using DI

Conclusion

Dependency injection helps C# applications separate responsibilities and reduce coupling between components. By using constructor injection, interfaces, and appropriate service lifetimes, developers can create applications that are easier to test, maintain, and extend.

Note: Note: Dependency injection is a design tool, not a goal by itself. Use it where managing dependencies externally improves flexibility, testing, or maintainability.