Introduction
Dependency Injection (DI) is a cornerstone of modern software architecture in .NET Core and beyond. It promotes loose coupling, testability, and maintainability, making applications easier to build and scale.
In this article, you’ll learn what Dependency Injection is, how it works in .NET Core, and how to manage it efficiently—from basic service registration to advanced patterns like scoped lifetimes and third-party containers.
๐ฑ What is Dependency Injection?
Dependency Injection is a design pattern that allows classes to receive their dependencies from external sources rather than creating them internally. It follows the Inversion of Control (IoC) principle.
Why use DI?
- Reduces tight coupling
- Increases testability (easy to mock dependencies)
- Promotes code reuse and cleaner architecture
โ๏ธ How Dependency Injection Works in .NET Core
.NET Core comes with a built-in dependency injection container out of the box, and it’s integrated into the ASP.NET Core pipeline.
Step 1. Register Services
You register services in the Program.cs
file (or Startup.cs
in older versions):
builder.Services.AddTransient<IMyService, MyService>();
builder.Services.AddScoped<IRepository, Repository>();
builder.Services.AddSingleton<ILogger, Logger>();
Step 2. Inject Services
Services are injected via constructor injection (recommended):
public class HomeController : Controller
{
private readonly IMyService _service;
public HomeController(IMyService service)
{
_service = service;
}
public IActionResult Index()
{
var data = _service.GetData();
return View(data);
}
}
๐ Understanding Service Lifetimes
.NET Core supports three built-in lifetimes:
Lifetime |
Description |
Use Case |
Transient |
New instance every time |
Lightweight, stateless services |
Scoped |
One instance per request |
Services with request-specific state |
Singleton |
One instance for the whole app |
Shared/global services like caching |
services.AddTransient<IMyService, MyService>();
services.AddScoped<IUserSession, UserSession>();
services.AddSingleton<IConfigProvider, ConfigProvider>();
๐งช Testing with Dependency Injection
DI makes testing easier by allowing you to inject mocked services:
var mockService = new Mock<IMyService>();
mockService.Setup(s => s.GetData()).Returns("Mocked Data");
var controller = new HomeController(mockService.Object);
๐งฐ Advanced Scenarios
1. Factory-Based Registration
services.AddSingleton<IMyService>(provider =>
{
var config = provider.GetRequiredService<IConfiguration>();
return new MyService(config["ApiKey"]);
});
2. Conditional Registration
if (env.IsDevelopment())
services.AddSingleton<IMyService, DevService>();
else
services.AddSingleton<IMyService, ProdService>();
3. Using Third-Party Containers (e.g., Autofac)
For more flexibility:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
.ConfigureContainer<ContainerBuilder>(builder =>
{
builder.RegisterType<MyService>().As<IMyService>();
});
๐ Best Practices for Managing DI in .NET Core
-
Prefer interfaces over concrete classes
-
Avoid service locator pattern (don’t use IServiceProvider.GetService<T>()
unless necessary)
-
Use scoped services for per-request data
-
Group registrations by module or feature
-
Use extension methods for cleaner Program.cs
public static class ServiceExtensions
{
public static IServiceCollection AddMyFeatureServices(this IServiceCollection services)
{
services.AddScoped<IMyService, MyService>();
return services;
}
}
โ
Conclusion
Dependency Injection in .NET Core is powerful, flexible, and easy to manage once you understand the fundamentals. Whether you're building small APIs or large-scale enterprise systems, proper DI practices will ensure your app stays modular and maintainable.