Dependency Injection in C# — Complete Guide
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
Captive dependencies: A scoped service injected into a Singleton becomes captive (it lives forever). Always consider lifetime compatibility.
Service Locator anti-pattern: Injecting
IServiceProviderdirectly instead of specific dependencies makes the API opaque.Disposing singletons incorrectly: Singletons are disposed when the container is disposed. Do not manually dispose singleton services.
Over-registration: Registering too many fine-grained services obscures the architecture. Group related functionality.
Not using TryAdd: When building libraries, use
TryAddSingleton,TryAddScoped, andTryAddTransientso consumers can override defaults.
Practice Questions
Create a
INotificationServiceinterface withSendmethod and implement it withEmailNotificationandSmsNotification. Register both using the DI container.Write a decorator pattern using DI that logs every method call on a service.
Refactor a tightly coupled class that creates its own database connection to use constructor injection.
Challenge: Implement a multi-tenant service resolver that returns different implementations based on the current tenant.
FAQ
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