ASP.NET Core has a built-in Dependency Injection (DI) container that creates objects and supplies their required dependencies automatically.
Instead of a class creating its own dependencies with new, the dependencies are registered in Program.cs and injected through the constructor.
Without dependency injection
public class ProductController : Controller
{
private readonly ProductService _service;
public ProductController()
{
_service = new ProductService();
}
}
Problems:
- The controller is tightly coupled to
ProductService. - Replacing the implementation is difficult.
- Unit testing with a mock service is difficult.
- The controller controls the dependency’s lifetime.
With dependency injection
1. Create an interface
public interface IProductService
{
Task<List<Product>> GetAllAsync();
}
2. Create the implementation
public class ProductService : IProductService
{
public async Task<List<Product>> GetAllAsync()
{
return await Task.FromResult(new List<Product>());
}
}
3. Register the service
Program.cs:
builder.Services.AddScoped<IProductService, ProductService>();
This registration tells the DI container:
When a class requests
IProductService, create and provideProductService.
4. Inject it into a controller
public class ProductController : Controller
{
private readonly IProductService _productService;
public ProductController(IProductService productService)
{
_productService = productService;
}
public async Task<IActionResult> Index()
{
var products = await _productService.GetAllAsync();
return View(products);
}
}
ASP.NET Core automatically:
- Receives the request.
- Creates
ProductController. - Finds that it requires
IProductService. - Locates the registered
ProductService. - Creates the service according to its configured lifetime.
- Passes it into the controller’s constructor.
- Disposes applicable objects when their lifetime ends.
Service lifetimes
| Lifetime | Instance created | Typical use |
|---|---|---|
Transient |
Every time the service is requested | Lightweight, stateless services |
Scoped |
Once per HTTP request | Business services, EF Core DbContext |
Singleton |
Once for the application lifetime | Shared thread-safe services |
Transient
builder.Services.AddTransient<
IEmailFormatter,
EmailFormatter>();
A new EmailFormatter is created every time it is resolved.
Scoped
builder.Services.AddScoped<
IProductService,
ProductService>();
The same instance is reused throughout one HTTP request. A different request receives a new instance.
Singleton
builder.Services.AddSingleton<
ISystemClock,
SystemClock>();
One instance is shared by all requests and users until the application stops.
Singleton services must be thread-safe and should not store user-specific mutable data.
Nested dependencies
The container resolves the complete dependency graph automatically:
public class ProductService : IProductService
{
private readonly IProductRepository _repository;
private readonly ILogger<ProductService> _logger;
public ProductService(
IProductRepository repository,
ILogger<ProductService> logger)
{
_repository = repository;
_logger = logger;
}
}
Registrations:
builder.Services.AddScoped<
IProductService,
ProductService>();
builder.Services.AddScoped<
IProductRepository,
ProductRepository>();
Resolution flow:
ProductController
↓ requires
IProductService → ProductService
↓ requires
IProductRepository → ProductRepository
↓
ILogger<ProductService>
ILogger<T> is registered by the framework, so it does not require a manual registration.
Common registration methods
Register interface and implementation:
builder.Services.AddScoped<
IProductService,
ProductService>();
Register a concrete class directly:
builder.Services.AddTransient<EmailService>();
Register an existing instance:
builder.Services.AddSingleton(
new ApplicationSettings
{
ApplicationName = "My MVC Application"
});
Register using a factory:
builder.Services.AddScoped<IReportService>(provider =>
{
var logger =
provider.GetRequiredService<ILogger<ReportService>>();
return new ReportService(logger, "PDF");
});
Framework registrations often use extension methods:
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<AppDbContext>();
builder.Services.AddMemoryCache();
builder.Services.AddHttpClient();
Important lifetime rule
A longer-lived service must not directly capture a shorter-lived service.
Incorrect:
builder.Services.AddSingleton<ReportService>();
builder.Services.AddScoped<AppDbContext>();
public class ReportService
{
public ReportService(AppDbContext dbContext)
{
}
}
ReportService lives for the entire application, while AppDbContext should live for only one request. This is called a captive dependency.
Use the same or a shorter consumer lifetime:
builder.Services.AddScoped<ReportService>();
builder.Services.AddScoped<AppDbContext>();
DI objects
IServiceCollection: Stores service registrations during startup.IServiceProvider: Resolves registered services at runtime.- Service scope: Controls the lifetime of scoped services.
Manual resolution is available:
var service =
app.Services.GetRequiredService<MySingletonService>();
However, constructor injection is generally preferred because dependencies remain explicit and classes are easier to test.
Key points
- ASP.NET Core provides DI without requiring an external library.
- Register services in
builder.Services. - Prefer constructor injection.
- Program against interfaces when interchangeable implementations or mocking are useful.
- Choose
Transient,ScopedorSingletoncarefully. DbContextis normally Scoped.- Singleton services must be thread-safe.
- Avoid injecting Scoped services into Singleton services.
- DI improves loose coupling, maintainability and unit testing.