Skip to content

Dependency Injection in C# — Complete Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Dependency Injection in C#. We cover key concepts, practical examples, and best practices to help you master this topic.

Hook

Dependency injection (DI) is a design pattern that inverts the responsibility of creating dependencies. Instead of a class creating its own dependencies, they are provided from the outside. The .NET ecosystem includes a built-in DI container that makes this pattern easy to adopt across all application types.

Learning Path

graph LR
  A[DI Concepts] --> B[Constructor Injection]
  A --> C[Service Lifetime]
  B --> D[DI Container]
  D --> E[Registration]
  D --> F[Resolution]
  style A fill:#4a90d9,color:#fff
  style B fill:#4a90d9,color:#fff
  style C fill:#4a90d9,color:#fff
  style D fill:#4a90d9,color:#fff
  style E fill:#4a90d9,color:#fff
  style F fill:#4a90d9,color:#fff

Why Dependency Injection

Without DI, classes are tightly coupled to their dependencies.

// Tight coupling - hard to test, hard to change
public class OrderService
{
    private readonly DatabaseLogger _logger = new DatabaseLogger();

    public void ProcessOrder(Order order)
    {
        // business logic
        _logger.Log("Order processed");
    }
}

With DI, dependencies are injected through the constructor.

// Loose coupling - testable, flexible
public class OrderService
{
    private readonly ILogger _logger;

    public OrderService(ILogger logger)
    {
        _logger = logger;
    }

    public void ProcessOrder(Order order)
    {
        Console.WriteLine("Processing order...");
        _logger.Log("Order processed");
    }
}

The Built-in DI Container

C# applications can use Microsoft.Extensions.DependencyInjection.

using Microsoft.Extensions.DependencyInjection;

// Define abstractions
public interface ILogger
{
    void Log(string message);
}

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

// Implementations
public class ConsoleLogger : ILogger
{
    public void Log(string message) =>
        Console.WriteLine($"[LOG] {message}");
}

public class SmtpEmailService : IEmailService
{
    public void Send(string to, string subject) =>
        Console.WriteLine($"Sending email to {to}: {subject}");
}

// Service collection setup
var services = new ServiceCollection();
services.AddSingleton<ILogger, ConsoleLogger>();
services.AddTransient<IEmailService, SmtpEmailService>();
services.AddTransient<OrderService>();

var provider = services.BuildServiceProvider();
var orderService = provider.GetRequiredService<OrderService>();
orderService.ProcessOrder(new Order { Id = 1 });

Service Lifetimes

Choose the right lifetime for each service.

// Singleton: one instance for the entire application lifetime
services.AddSingleton<ILogger, ConsoleLogger>();

// Transient: a new instance every time it is requested
services.AddTransient<IEmailService, SmtpEmailService>();

// Scoped: one instance per scope (per request in web apps)
services.AddScoped<IDbContext, AppDbContext>();
public class LifetimeDemo
{
    private readonly ILogger _logger1;
    private readonly ILogger _logger2;
    private readonly IEmailService _email1;
    private readonly IEmailService _email2;

    public LifetimeDemo(ILogger logger1, ILogger logger2,
        IEmailService email1, IEmailService email2)
    {
        _logger1 = logger1;
        _logger2 = logger2;
        _email1 = email1;
        _email2 = email2;
    }

    public void ShowLifetimes()
    {
        // Singleton: same instance
        Console.WriteLine($"Same logger: {ReferenceEquals(_logger1, _logger2)}");

        // Transient: different instances
        Console.WriteLine($"Same email service: {ReferenceEquals(_email1, _email2)}");
    }
}

Output:

Same logger: True
Same email service: False

Registration Patterns

There are several ways to register services.

// Register by interface and implementation
services.AddSingleton<ILogger, ConsoleLogger>();

// Register by implementation type only
services.AddSingleton<FileLogger>();

// Register instance directly
services.AddSingleton<ILogger>(new ConsoleLogger());

// Register with factory
services.AddSingleton<ILogger>(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    return new FileLogger(config["LogPath"]);
});

// Register open generics
services.AddSingleton(typeof(IRepository<>), typeof(EfRepository<>));

DI in ASP.NET Core

Web applications get DI automatically.

// Program.cs (Minimal API)
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddTransient<IEmailService, SmtpEmailService>();

var app = builder.Build();

app.MapGet("/products", async (IProductRepository repo) =>
{
    return await repo.GetAllAsync();
});

app.Run();

For MVC controllers, dependencies are injected via the constructor.

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductRepository _repo;

    public ProductsController(IProductRepository repo)
    {
        _repo = repo;
    }

    [HttpGet]
    public async Task<IActionResult> GetAll() =>
        Ok(await _repo.GetAllAsync());
}

Common Mistakes

  1. Captive dependencies: A scoped service injected into a Singleton becomes captive (it lives forever). Always consider lifetime compatibility.

  2. Service Locator anti-pattern: Injecting IServiceProvider directly instead of specific dependencies makes the API opaque.

  3. Disposing singletons incorrectly: Singletons are disposed when the container is disposed. Do not manually dispose singleton services.

  4. Over-registration: Registering too many fine-grained services obscures the architecture. Group related functionality.

  5. Not using TryAdd: When building libraries, use TryAddSingleton, TryAddScoped, and TryAddTransient so consumers can override defaults.

Practice Questions

  1. Create a INotificationService interface with Send method and implement it with EmailNotification and SmsNotification. Register both using the DI container.

  2. Write a decorator pattern using DI that logs every method call on a service.

  3. Refactor a tightly coupled class that creates its own database connection to use constructor injection.

  4. Challenge: Implement a multi-tenant service resolver that returns different implementations based on the current tenant.

FAQ

Is the built-in DI container enough for production?

Yes, the Microsoft.Extensions.DependencyInjection container is production-ready and used by ASP.NET Core, EF Core, and other Microsoft frameworks.

What is the difference between AddTransient, AddScoped, and AddSingleton?

Transient creates a new instance each time. Scoped creates one instance per scope (request). Singleton creates one instance for the entire application lifetime.

Can I use DI without ASP.NET Core?

Yes, DI is available as a NuGet package (Microsoft.Extensions.DependencyInjection) and works in console apps, WPF, MAUI, and background services.

How do I handle disposable services?

The container automatically disposes of IDisposable and IAsyncDisposable services it creates. Do not dispose them manually.

Should I use a third-party container instead?

The built-in container covers most scenarios. Consider alternatives like Autofac or StructureMap for advanced features like property injection, interception, or modules.

Mini Project: Email Notification System

Build a notification system that sends messages through different channels using DI.

using Microsoft.Extensions.DependencyInjection;

public interface INotifier
{
    void Notify(string recipient, string message);
}

public class EmailNotifier : INotifier
{
    public void Notify(string recipient, string message) =>
        Console.WriteLine($"EMAIL to {recipient}: {message}");
}

public class SmsNotifier : INotifier
{
    public void Notify(string recipient, string message) =>
        Console.WriteLine($"SMS to {recipient}: {message}");
}

public class NotificationService
{
    private readonly IEnumerable<INotifier> _notifiers;

    public NotificationService(IEnumerable<INotifier> notifiers)
    {
        _notifiers = notifiers;
    }

    public void SendAll(string recipient, string message)
    {
        foreach (var notifier in _notifiers)
        {
            notifier.Notify(recipient, message);
        }
    }
}

var services = new ServiceCollection();
services.AddTransient<INotifier, EmailNotifier>();
services.AddTransient<INotifier, SmsNotifier>();
services.AddTransient<NotificationService>();

var provider = services.BuildServiceProvider();
var notificationService = provider.GetRequiredService<NotificationService>();
notificationService.SendAll("user@example.com", "Welcome to our service!");

Output:

EMAIL to user@example.com: Welcome to our service!
SMS to user@example.com: Welcome to our service!

Dependency injection transforms tightly coupled C# code into modular, testable, and maintainable systems. The built-in .NET DI container makes it accessible for projects of any size.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro