← Back to Articles         
Built-in Dependency Injection in .NET

Built-in Dependency Injection in .NET

Published on 27 Sep 2026     4 min read ASP.NET Core MVC
MVC

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 provide ProductService.

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:

  1. Receives the request.
  2. Creates ProductController.
  3. Finds that it requires IProductService.
  4. Locates the registered ProductService.
  5. Creates the service according to its configured lifetime.
  6. Passes it into the controller’s constructor.
  7. 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, Scoped or Singleton carefully.
  • DbContext is normally Scoped.
  • Singleton services must be thread-safe.
  • Avoid injecting Scoped services into Singleton services.
  • DI improves loose coupling, maintainability and unit testing.
← Back to Articles