ASP.NET Core  

Dependency Injection Deep Dive in ASP.NET Core

Introduction

Modern software applications are built with focus on flexibility, maintainability, and testability. In ASP.NET Core, one of the most powerful features that supports these goals is Dependency Injection (DI). Understanding DI properly will help you write cleaner, modular, and scalable applications.

Many developers know the definition of DI but struggle to understand how it actually works internally, why it is needed, how to use it correctly, and where it fits in a real ASP.NET Core project.

This deep-dive article explains DI in a clear, simple, and practical way:

  • What Dependency Injection is

  • Why it matters

  • Types of dependencies

  • Problems without DI

  • How DI works internally in ASP.NET Core

  • Service lifetime explained

  • Constructor, Method, and Property Injection

  • Using DI with interfaces, repositories, services, and controllers

  • Common mistakes and best practices

  • Advanced DI patterns

  • Practical real-world examples

By the end of this guide, you will feel confident using DI in any project.

What Is Dependency Injection?

Simple Definition

Dependency Injection is a design pattern where a class receives the objects it depends on rather than creating them internally.

In simple terms:

Instead of:

  • Class A creating Class B inside itself

We do:

  • Class B is given (injected) into Class A by the framework

This reduces coupling and increases flexibility.

Why Is It Important?

DI ensures:

  • Better code structure

  • Loose coupling between classes

  • Easier unit testing

  • Simple implementation replacement

  • Clean architecture

  • Improved maintainability

  • Better separation of concerns

ASP.NET Core uses DI as a built-in feature, unlike older .NET frameworks that required external libraries.

Understanding Dependencies

A dependency is any object that another object needs to perform its work.

Example:

public class OrderService
{
    private readonly PaymentService _payment;

    public OrderService()
    {
        _payment = new PaymentService();
    }
}

Here:

  • OrderService depends on PaymentService

  • OrderService creates PaymentService internally

  • This is called tight coupling

This design has multiple problems.

Problems Without Dependency Injection

When we do not use DI, we face several issues:

1. Tight Coupling

OrderService becomes tightly tied to PaymentService.
If PaymentService changes, OrderService must also change.

2. Hard to Replace Implementation

What if you want CashPaymentService tomorrow instead of PaymentService?

Without DI, you must edit the code:

_payment = new CashPaymentService();

This breaks Open/Closed Principle.

3. Difficult Unit Testing

Tests require mocks, but tight coupling makes it impossible.

4. Hard to Manage Large Applications

With 20 or more services and repositories, managing dependencies becomes chaotic.

5. Violates SOLID Principles

Especially:

  • DIP (Dependency Inversion Principle)

  • OCP (Open/Closed Principle)

  • SRP (Single Responsibility Principle)

DI Solves These Problems

With DI:

  • Classes no longer create dependencies.

  • The framework creates them and passes them in.

  • Everything depends on interfaces, not classes.

  • You can swap implementations easily.

  • Testing becomes simple with mocks.

Let us rewrite the earlier example using DI.

How DI Works in ASP.NET Core

ASP.NET Core provides a built-in IoC (Inversion of Control) container.

The DI flow in ASP.NET Core:

  1. Register dependencies in Program.cs

  2. Framework creates objects automatically

  3. Framework manages lifetime

  4. Framework injects dependencies when controller or service needs them

This means you never manually create objects.
ASP.NET Core handles it internally.

Registering Services in Program.cs

Basic example:

builder.Services.AddScoped<IPaymentService, PaymentService>();

This means:

  • Whenever someone needs IPaymentService

  • Provide PaymentService instance

Constructor Injection

This is the most common DI method in ASP.NET Core and the recommended one.

Example:

public class OrderService
{
    private readonly IPaymentService _payment;

    public OrderService(IPaymentService payment)
    {
        _payment = payment;
    }
}

ASP.NET Core creates PaymentService and injects it automatically.

Method Injection

You pass dependencies as method arguments:

public void ProcessOrder(IPaymentService payment)
{
    payment.Pay(100);
}

Used rarely, mostly in helper methods.

Property Injection

Dependency set using property:

public class ReportService
{
    public ILogger Logger { get; set; }
}

Not very common in ASP.NET Core. Not recommended for most cases.

Service Lifetime Explained

The service lifetime defines how long an instance exists.

ASP.NET Core supports three types:

1. Transient (AddTransient)

  • New instance every time it is requested

  • Lightweight services

  • Stateless services

Example:

builder.Services.AddTransient<IEmailService, EmailService>();

2. Scoped (AddScoped)

  • One instance per request

  • Best for repositories and database contexts

Example:

builder.Services.AddScoped<IOrderRepository, OrderRepository>();

3. Singleton (AddSingleton)

  • One instance for the entire application

  • Used for configuration, caching, logging

Example:

builder.Services.AddSingleton<ICacheService, MemoryCacheService>();

Real-world Usage in a Professional ASP.NET Core Application

Let us build a small sample architecture.

Step 1: Define Interfaces

public interface IProductRepository
{
    IEnumerable<Product> GetAll();
}

public interface IPaymentService
{
    void Pay(decimal amount);
}

public interface IEmailService
{
    void SendEmail(string to, string message);
}

Step 2: Implement the Interfaces

ProductRepository.cs:

public class ProductRepository : IProductRepository
{
    public IEnumerable<Product> GetAll()
    {
        return new List<Product>
        {
            new Product {Id = 1, Name = "Laptop"},
            new Product {Id = 2, Name = "Mobile"}
        };
    }
}

PaymentService.cs:

public class PaymentService : IPaymentService
{
    public void Pay(decimal amount)
    {
        Console.WriteLine("Payment completed.");
    }
}

EmailService.cs:

public class EmailService : IEmailService
{
    public void SendEmail(string to, string message)
    {
        Console.WriteLine("Email sent.");
    }
}

Step 3: Register Services in Program.cs

builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddTransient<IPaymentService, PaymentService>();
builder.Services.AddSingleton<IEmailService, EmailService>();

Step 4: Inject Into Controller

ProductController.cs:

[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
    private readonly IProductRepository _repo;
    private readonly IPaymentService _payment;
    private readonly IEmailService _email;

    public ProductController(IProductRepository repo, 
                             IPaymentService payment, 
                             IEmailService email)
    {
        _repo = repo;
        _payment = payment;
        _email = email;
    }

    [HttpGet]
    public IActionResult GetProducts()
    {
        var products = _repo.GetAll();
        _payment.Pay(500);
        _email.SendEmail("[email protected]", "Thanks for shopping.");

        return Ok(products);
    }
}

This is a complete DI-enabled architecture.

Internals of ASP.NET Core Dependency Injection

ASP.NET Core uses a built-in IoC container.

What happens internally:

  1. When the application starts, it builds a ServiceProvider.

  2. ServiceProvider contains all registered services.

  3. When a controller is created, ASP.NET Core checks its constructor.

  4. It reads required dependencies.

  5. It fetches or creates the required services.

  6. It injects them into the controller.

Developers do not manage object creation manually.

DI with Repository Pattern and EF Core

Typical registration:

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddScoped<IProductRepository, ProductRepository>();

Repository constructor:

public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;

    public ProductRepository(ApplicationDbContext context)
    {
        _context = context;
    }
}

This is the standard way DI is used with Entity Framework Core.

DI with Unit of Work

If your project uses Unit of Work:

builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

UnitOfWork constructor:

public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _context;

    public IProductRepository Products { get; }

    public UnitOfWork(ApplicationDbContext context, IProductRepository products)
    {
        _context = context;
        Products = products;
    }
}

Again, DI manages everything.

Common Mistakes Developers Make with DI

1. Injecting Too Many Services in One Class

If more than 5–6 dependencies are injected, your class has too many responsibilities.

2. Using AddSingleton for EF Core

EF Core DbContext must always be scoped.
Singleton will cause application crash.

3. Using Concrete Class Instead of Interface

Bad:

builder.Services.AddScoped<ProductRepository>();

Good:

builder.Services.AddScoped<IProductRepository, ProductRepository>();

4. Circular Dependencies

A depends on B, and B depends on A.
This must be avoided.

5. Injecting DbContext into other DbContexts

Leads to large messy systems.

Advanced DI Patterns in ASP.NET Core

1. Factory Pattern with DI

Useful when multiple implementations exist.

Example:

  • UPI payment

  • Card payment

  • Wallet payment

Use factory to resolve based on type.

2. Named Services

You can use IServiceProvider to fetch services dynamically.

3. Generics with DI

Example:

builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));

This allows DI for all repository types.

4. Options Pattern

Used for configuration:

builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("EmailSettings"));

Inject like:

public EmailService(IOptions<EmailSettings> options)
{
    _settings = options.Value;
}

5. Middleware Dependency Injection

Custom middleware can receive DI via constructor.

Benefits of DI in Real ASP.NET Core Projects

  1. Cleaner and modular code

  2. Easier to test using mocks

  3. Supports SOLID principles

  4. Reduces tight coupling

  5. Makes system more maintainable

  6. Helps in scaling the application

  7. Supports better architecture (Repository, Services, UoW, CQRS)

  8. Improves readability

  9. Allows multiple implementations

  10. Improves future maintainability

Summary

Dependency Injection is not just a technical concept; it is a fundamental architectural tool that makes ASP.NET Core applications clean, scalable, flexible, and professional.

In this deep dive, we covered:

  • What DI is

  • Why DI is essential

  • How DI works internally

  • How services are registered

  • Constructor, method, and property injection

  • Service lifetimes

  • Practical examples

  • DI with repository and Unit of Work

  • Advanced patterns

  • Best practices

Mastering DI will dramatically improve the quality of your ASP.NET Core applications and help you design enterprise-level architectures confidently.