Dependency Injection Basics in C#
Source: Dev.to
What is Dependency Injection?
Dependency Injection (DI) is a design pattern that supplies a class with the objects it needs from the outside rather than creating them internally. This makes code more flexible, testable, and maintainable. In the C# and .NET Core ecosystem, DI is supported out of the box.
Example Without DI
public class OrderService
{
private readonly Logger _logger = new Logger();
public void CreateOrder(string product)
{
_logger.Log($"Order created: {product}");
}
}
public class Logger
{
public void Log(string message) => Console.WriteLine(message);
}
In this approach OrderService is tightly coupled to Logger. Replacing the logger requires modifying OrderService.
Example With DI
public interface ILogger
{
void Log(string message);
}
public class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine("[Console] " + message);
}
public class FileLogger : ILogger
{
public void Log(string message) =>
System.IO.File.AppendAllText("log.txt", message + "\n");
}
public class OrderService
{
private readonly ILogger _logger;
// Constructor Injection
public OrderService(ILogger logger)
{
_logger = logger;
}
public void CreateOrder(string product)
{
_logger.Log($"Order created: {product}");
}
}
class Program
{
static void Main()
{
// Different loggers can be selected
ILogger logger = new ConsoleLogger();
var service = new OrderService(logger);
service.CreateOrder("Laptop");
}
}
OrderService now depends on the ILogger interface, allowing any implementation (e.g., ConsoleLogger or FileLogger) to be swapped without changing the service code.
Types of Injection
- Constructor Injection – dependencies are provided through the constructor (most common).
- Property Injection – dependencies are assigned via public properties.
- Method Injection – dependencies are passed as method parameters.
DI in .NET Core
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddScoped();
builder.Services.AddScoped();
var app = builder.Build();
app.MapGet("/order", (OrderService service) =>
{
service.CreateOrder("Phone");
return "Order processed.";
});
app.Run();
The built‑in DI container automatically creates and injects a ConsoleLogger instance for OrderService.
Service Lifetimes
When registering services, choose a lifetime that matches the intended usage:
| Lifetime | Description |
|---|---|
| Transient | A new instance is created every time it is requested. |
| Scoped | A single instance is created per HTTP request. |
| Singleton | A single instance is created at application start and reused throughout the app’s lifetime. |
// Transient
builder.Services.AddTransient();
// Scoped
builder.Services.AddScoped();
// Singleton
builder.Services.AddSingleton();
- Use Transient for lightweight, stateless services.
- Use Scoped for request‑based services (e.g.,
DbContext). - Use Singleton for shared, application‑wide services.
Benefits of Dependency Injection
- Loose Coupling – classes depend on abstractions (interfaces) rather than concrete implementations.
- Testability – easier to substitute mocks or fakes in unit tests.
- Flexibility – different implementations can be swapped without modifying dependent code.
- Maintainability – centralised management of dependencies improves readability and reduces duplication.